Compare commits

...

71 Commits

Author SHA1 Message Date
Laurent Nguyen
77f895d343 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-28 11:19:33 +02:00
Laurent Nguyen
92073a5646 Start kernel when opening notebook tab (#168)
* Add Auto-start kernel epic

* Replace deprecated rxjs empty() with EMPTY constant

* Fix format
2020-08-28 09:51:44 +02:00
Laurent Nguyen
3bc2701356 Format and fix type issues 2020-08-27 16:43:39 +02:00
Laurent Nguyen
35dbaeea96 Add code completion implementation 2020-08-27 15:52:38 +02:00
Laurent Nguyen
18745a9ae6 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-27 09:13:52 +02:00
Jordi Bunster
5c84b3a7d4 Allow 'platform' only to be overriden (#167)
ConfigContext defines all kinds of URLs and what not, I'm not
sure about the security implications of allowing all this stuff to
be modifiable by just anyone.
2020-08-25 22:48:58 -07:00
victor-meng
3223ff7685 Move createDatabase to RP (#166) 2020-08-25 15:45:37 -07:00
Steve Faulkner
38732af907 Triple equal lint rule (#162) 2020-08-21 19:51:36 -05:00
Steve Faulkner
e837f574a8 Fix Telemetry for String Case (#163) 2020-08-21 14:38:30 -05:00
victor-meng
47a5c315b5 Move updateCollection to ARM (#152) 2020-08-21 11:24:01 -07:00
victor-meng
1c80ced259 Fix settings tab issue for cassandra and add feature flag (#159)
* Fix settings tab issue for cassandra and add feature flag

* Adding the rest of the changes....

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2020-08-19 18:11:43 -05:00
victor-meng
5e6ac78b7d Tables - ReadCollection: Switch back to using SDK (#157) 2020-08-19 17:54:23 -05:00
Steve Faulkner
999196193f Update Config context ARM endpoint (#158) 2020-08-19 13:20:10 -05:00
victor-meng
951289e190 Switch back to SDK for deleteDatabase and use RP for readCollections for table API (#155) 2020-08-18 11:04:37 -07:00
Tanuj Mittal
3279460cfd Update @azure/cosmos to 3.9.0 (#154) 2020-08-17 14:26:19 -07:00
victor-meng
07b9c1d1b7 Switch back to using SDK to read databases and collections for Mongo API (#153) 2020-08-17 11:42:02 -07:00
Tanuj Mittal
dde2ca75c4 Show submit button by default in GenericRightPaneComponent & other fixes (#144)
* Bug fixes

* Fix build
2020-08-17 11:39:11 -07:00
Laurent Nguyen
f44a3da568 Allow boolean partition key in graph visualization (#150)
* Allow boolean pk values to be displayed

* Add unit tests
2020-08-17 09:50:57 +02:00
Tanuj Mittal
22b2e1df48 Support empty offers for serverless accounts (#132)
* Add support for no offers when serverless accounts

* Add workaround for ignoring failed /offers when using connection string

* Revert @azure/cosmos upgrade
2020-08-14 17:45:13 -07:00
Steve Faulkner
2752d6af00 Create Cassandra Keyspace via ARM (#142) 2020-08-13 13:26:52 -05:00
Laurent Nguyen
5be6f982f9 Resolve merge conflict 2020-08-13 16:35:26 +02:00
Laurent Nguyen
4fc9393b76 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-13 16:30:46 +02:00
Laurent Nguyen
cb5fe5316e Fix handling numeric partition keys (#113)
* Fix pk extraction from documentId in g.V() case

* Add unit test, cleanup pk related unit tests
2020-08-13 12:00:11 +02:00
Steve Faulkner
c0ce637eec Turn off HMR (#147) 2020-08-12 20:12:31 -05:00
victor-meng
b61a235bf6 Move readCollection, readCollections, and readDatabases to ARM (#134) 2020-08-12 17:13:43 -07:00
Steve Faulkner
0fa97c2ce9 Increase Cypress Timeout and Disable LiveReload in test (#145)
Co-authored-by: Tanuj Mittal <tamitta@microsoft.com>
2020-08-12 15:25:53 -05:00
victor-meng
fb71fb4e82 Refactor GenericRightPaneComponent to accept a ReactComponent as its content (#146) 2020-08-12 11:41:19 -07:00
victor-meng
455722c316 Fix deleteDatabase and deleteCollection with ARM (#143) 2020-08-11 18:36:42 -07:00
Tanuj Mittal
5886db81e9 Copy To functionality for notebooks (#141)
* Add Copy To functionality for notebooks

* Fix formatting

* Fix linting errors

* Fixes

* Fix build failure

* Rebase and address feedback

* Increase test coverage
2020-08-11 09:27:57 -07:00
Srinath Narayanan
7a3e54d43e Added support for acknowledging code of conduct for using public Notebook Gallery (#117)
* minro code edits

* Added support for acknowledging code of conduct

- Added CodeOfConduct component that shows links and a checkbox that must be acknwledged before seeing the public galley
- Added verbose message for notebook publish error (when another notebook with the same name exists in the gallery)
- Added a feature flag for enabling code of conduct acknowledgement

* Added Info Component

* minor edit

* fixed failign tests

* publish tab displayed only when code of conduct accepted

* added code of conduct fetch during publish

* fixed bug

* added test and addressed PR comments

* changed line endings

* added comment

* addressed PR comments
2020-08-11 00:37:05 -07:00
Steve Faulkner
3051961093 Add subscriptionId and authType to telemetry (#140) 2020-08-10 18:43:45 -05:00
Vignesh Rangaishenvi
abce15a6b2 Add init message when warming up notebook workspace (#139)
* Add init message

* Address comments
2020-08-10 15:02:24 -07:00
Steve Faulkner
a5b824ebb5 Fix master compile error 2020-08-10 12:02:18 -05:00
victor-meng
e28765d740 Add null check when reading offerAutopilotSettings (#138) 2020-08-10 11:55:43 -05:00
Srinath Narayanan
95f1efc03f Added support for notebook viewer link injection (#124)
* Added support for notebook viewer link injection

* updated tests
2020-08-10 01:53:51 -07:00
Steve Faulkner
455a6ac81b Fix update offer beyond throughput limit error (#135) 2020-08-06 18:15:40 -05:00
Vignesh Rangaishenvi
08ee86ecf1 Fix connection string renew token pane (#136)
* Fix IcM issue + conn string parsing

* format code

* Undo fix for IcM issue
2020-08-06 16:15:31 -07:00
Steve Faulkner
0011007d5f Refactor Global state into Context Files (#128) 2020-08-06 14:03:46 -05:00
vchske
d45af21996 Updating error message on mongo collection create (#118)
* 1) Updated mongo collection create pane to display a better error when a shard key is ill formed.
2) Updated an error message to use double quotes since no string interpolation is used. I thought it was included in a previoue PR but I guess the change didn't get staged.
2020-08-06 12:56:40 -05:00
Steve Faulkner
a64109ebaa Fix Generated ARM types (#131) 2020-08-06 09:27:41 -05:00
victor-meng
70f9b28499 Fix tabs manager test (#130) 2020-08-05 18:01:13 -07:00
Steve Faulkner
78e70cc7cc Refresh Caches only in Portal (#129) 2020-08-05 15:54:17 -05:00
Steve Faulkner
f132a8546c Move SQL database deletion to ARM (#126) 2020-08-04 18:03:14 -05:00
Tanuj Mittal
e6acf6686f Update NotebookReadOnlyRenderer.tsx (#127) 2020-08-04 10:24:25 -07:00
Steve Faulkner
2904a1a60d Move Delete Container call to use ARM when logged in with AAD (#110) 2020-08-03 17:11:07 -05:00
Tanuj Mittal
8c792fd147 Hide Azure Synapse Link button for Serverless accounts (#121) 2020-07-31 15:31:21 -07:00
Steve Faulkner
2a53dfabb5 Fix refresh resources button and remove portal specific notebooks call (#123) 2020-07-31 16:45:36 -05:00
Srinath Narayanan
14ef40029d Added session based view updation for gallery notebooks (#120) 2020-07-31 12:31:05 -07:00
Steve Faulkner
dab6e43d0d Hotfix: Remove extra JSON.stringify in Monogo update code path (#119) 2020-07-28 15:13:48 -05:00
Steve Faulkner
aea168c893 Add lint rule to prefer arror function (#114) 2020-07-27 16:40:04 -05:00
Steve Faulkner
fea321cd68 More ViewModel cleanup (#116) 2020-07-27 16:05:25 -05:00
Steve Faulkner
2e49ed45c3 Refactor DocumentClientUtilityBase to not be a class (#115) 2020-07-27 12:58:27 -05:00
Steve Faulkner
6d142f16f9 Refactor Data Access Utility (#112) 2020-07-24 16:45:48 -05:00
Steve Faulkner
6dcdacc8c4 Update CODEOWNERS 2020-07-24 15:45:42 -05:00
Tanuj Mittal
33969581ac Support serverless accounts (#109)
* Changes for serverless accounts

* Dont show upsell message for serverless accounts

* Update CassandraAddCollectionPane to support serverless
2020-07-24 13:13:54 -07:00
Srinath Narayanan
dc67c5f40b Added support for taking screenshot during Notebook publish to Gallery (#108)
* Added support for taking screenshot

- Screenshot is taken using html2canvas package
- Converted to base 64 and uploaded to metadata
- For Using first display output
  - Notebok object is passed instead of string, to publish pane
  - The first cell with output present is parsed out
  - The dom is also parsed to get corresponding div element to take screenshot of the first output

* fixed bug

* Addressed PR comments

- FIxed bug that didn't capture screenshot when mutiple notebook tabs are opened

* removed unnecessary dependencies

* fixed compile issues

* more edits
2020-07-23 00:43:53 -07:00
Steve Faulkner
acc65c9588 Update README.md 2020-07-22 13:52:47 -05:00
Steve Faulkner
6860d8db1c Remove unused Data Access methods (#107) 2020-07-22 12:03:51 -05:00
Steve Faulkner
3187756d03 Update README.md 2020-07-21 15:03:18 -05:00
Vignesh Rangaishenvi
0f4ff0e49f Support readers (#105) 2020-07-21 11:57:23 -07:00
Steve Faulkner
4f86015be7 Remove OpenActionsStubs (#106) 2020-07-21 13:50:51 -05:00
Steve Faulkner
46cca859e3 Add more files to strict mode (#93) 2020-07-21 12:14:55 -05:00
Steve Faulkner
574fdcaf37 Remove Pane Stubs (#102) 2020-07-21 09:23:51 -05:00
Steve Faulkner
eab6506940 Remove Explorer Stub and ViewModel.Explorer (#101) 2020-07-20 12:59:40 -05:00
Srinath Narayanan
050da28d6e Added support for custom image upload during publish to Gallery (#99)
* Added support for custom image upload

- Dropdown gives an option for URL or image upload
- Preview shows how the card will be displayed in the gallery
- base64 converted image stored in metadata document
- Max limit is 1.5MiB for the image

* fixed lint errors

* addressed PR comments

- Added test

* added snapshot

* fixed failing test
2020-07-17 15:32:39 -07:00
Steve Faulkner
ffae9baca2 Tabs CSS Hotfix and hash CSS file contents (#98)
Co-authored-by: Victor Meng <vimeng@microsoft.com>
2020-07-16 20:35:18 -05:00
Tanuj Mittal
e491c1a042 Use gallery.html instead of /gallery/index.html as endpoint (#94) 2020-07-15 23:41:05 -07:00
Steve Faulkner
444e25c086 More Spark UI Cleanup (#89)
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
2020-07-15 16:59:04 -05:00
Steve Faulkner
99c6a7ebcc Make explicit any an error (#81)
Co-authored-by: Steve Faulkner <stfaul@microsoft.com>
2020-07-15 16:21:50 -05:00
Laurent Nguyen
ee51e873b8 Fix build issues 2020-07-13 13:52:23 +02:00
Laurent Nguyen
206a8ef93b New Monaco Editor 2020-07-13 13:52:05 +02:00
303 changed files with 11953 additions and 9356 deletions

View File

@@ -1,5 +1,6 @@
**/node_modules/
dist/
Contracts/
src/Api/Apis.ts
src/AuthType.ts
src/Bindings/BindingHandlersRegisterer.ts
@@ -137,7 +138,6 @@ src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ClusterLibraryPane.ts
src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
@@ -145,7 +145,6 @@ src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/ExecuteSprocParamsPane.ts
src/Explorer/Panes/GraphStylingPane.ts
src/Explorer/Panes/LibraryManagePane.ts
src/Explorer/Panes/LoadQueryPane.ts
src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts
@@ -299,11 +298,9 @@ src/Utils/DatabaseAccountUtils.ts
src/Utils/JunoUtils.ts
src/Utils/MessageValidation.ts
src/Utils/NotebookConfigurationUtils.ts
src/Utils/NotificationConsoleUtils.ts
src/Utils/OfferUtils.test.ts
src/Utils/OfferUtils.ts
src/Utils/PricingUtils.test.ts
src/Utils/PricingUtils.ts
src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts
src/Utils/StringUtils.test.ts
@@ -331,10 +328,6 @@ src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx
src/Explorer/Controls/Directory/DirectoryListComponent.tsx
src/Explorer/Controls/Editor/EditorReact.tsx
src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
src/Explorer/Controls/LibraryManagement/ClusterLibraryGrid.tsx
src/Explorer/Controls/LibraryManagement/ClusterLibraryGridAdapter.tsx
src/Explorer/Controls/LibraryManagement/LibraryManage.tsx
src/Explorer/Controls/LibraryManagement/LibraryManageComponentAdapter.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx

View File

@@ -3,7 +3,7 @@ module.exports = {
browser: true,
es6: true
},
plugins: ["@typescript-eslint", "no-null"],
plugins: ["@typescript-eslint", "no-null", "prefer-arrow"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: {
Atomics: "readonly",
@@ -39,6 +39,9 @@ module.exports = {
curly: "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-extraneous-class": "error",
"no-null/no-null": "error"
"no-null/no-null": "error",
"@typescript-eslint/no-explicit-any": "error",
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error"
}
};

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @Azure/cosmos-explorer-owners
* @Azure/cosmos-explorer-owners @Azure/azure-cosmos-explorer-developers

View File

@@ -1,9 +1,13 @@
name: CI
on:
push:
branches: [master]
branches:
- master
- hotfix/*
- release/*
pull_request:
branches: [master]
branches:
- master
jobs:
compile:
runs-on: ubuntu-latest
@@ -52,6 +56,7 @@ jobs:
- run: npm run test
build:
runs-on: ubuntu-latest
needs: [lint, format, compile, unittest]
name: "Build"
steps:
- uses: actions/checkout@v2
@@ -75,6 +80,7 @@ jobs:
path: dist/
endtoendemulator:
name: "End To End Tests | Emulator | SQL"
needs: [lint, format, compile, unittest]
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
@@ -101,6 +107,7 @@ jobs:
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
endtoendsql:
name: "End To End Tests | SQL"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -127,8 +134,14 @@ jobs:
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
- uses: actions/upload-artifact@v2
name: videos
if: ${{ failure() }}
with:
path: "**/*.mp4"
endtoendmongo:
name: "End To End Tests | Mongo"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -155,8 +168,14 @@ jobs:
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
- uses: actions/upload-artifact@v2
if: ${{ failure() }}
name: videos
with:
path: "**/*.mp4"
accessibility:
name: "Accessibility | Hosted"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -179,7 +198,7 @@ jobs:
NODE_TLS_REJECT_UNAUTHORIZED: 0
nuget:
name: Publish Nuget
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest
env:
@@ -203,7 +222,7 @@ jobs:
path: "*.nupkg"
nugetmpac:
name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest
env:

View File

@@ -1,5 +1,9 @@
# CosmosDB Explorer
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
![](https://sdkctlstore.blob.core.windows.net/exe/dataexplorer.gif)
## Getting Started
- `npm install`
@@ -87,6 +91,10 @@ Jest and Puppeteer are used for end to end production runners and are contained
1. Copy .env.example to .env and fill in all variables
2. Run `npm run test:e2e`
### Releasing
We generally adhear to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
# Contributing
Please read the [contribution guidelines](./CONTRIBUTING.md).

View File

@@ -3,7 +3,7 @@
"pluginsFile": false,
"fixturesFolder": false,
"supportFile": "./support/index.js",
"defaultCommandTimeout": 60000,
"defaultCommandTimeout": 90000,
"chromeWebSecurity": false,
"reporter": "mochawesome",
"reporterOptions": {

View File

@@ -6,7 +6,7 @@
"scripts": {
"test": "cypress run",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"test:sql": "cypress run --browser chrome --headless --spec \"./integration/dataexplorer/SQL/*\"",
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
"test:debug": "cypress open"
},

View File

@@ -39,10 +39,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 18,
functions: 22,
lines: 28,
statements: 27
branches: 20,
functions: 24,
lines: 30,
statements: 29.0
}
},

View File

@@ -2349,9 +2349,9 @@ a:link {
text-decoration: none;
}
.tabsContainer {
.tabsManagerContainer {
height: 100%;
width: 100%;
flex-grow: 1;
overflow: hidden;
}

261
package-lock.json generated
View File

@@ -5,13 +5,14 @@
"requires": true,
"dependencies": {
"@azure/cosmos": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.4.tgz",
"integrity": "sha512-IbSEadapQDajSCXj7gUc8OklkOd/oAY4w7XBLHouWc4iKQTtntb2DmGjhrbh2W5Ku+pmBSr1GTApCjQ55iIjlQ==",
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.9.0.tgz",
"integrity": "sha512-SA+QB54I8Dvg/ZolHpsEDLK/sbSB9sFmSU1ElnMTFw88TVik+LYHq4o/srU2TY6Gr1BketjPmgLVEqrmnRvjkw==",
"requires": {
"@types/debug": "^4.1.4",
"debug": "^4.1.1",
"fast-json-stable-stringify": "^2.0.0",
"jsbi": "^3.1.3",
"node-abort-controller": "^1.0.4",
"node-fetch": "^2.6.0",
"os-name": "^3.1.0",
@@ -22,14 +23,14 @@
},
"dependencies": {
"tslib": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
},
"uuid": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
}
}
},
@@ -6024,6 +6025,16 @@
"natural-compare": "^1.4.0",
"pretty-format": "^24.9.0",
"semver": "^6.2.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"jest-validate": {
@@ -7577,11 +7588,40 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"@types/mkdirp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz",
"integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "12.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz",
"integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A=="
},
"@types/node-fetch": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz",
"integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==",
"requires": {
"@types/node": "*",
"form-data": "^3.0.0"
},
"dependencies": {
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -7681,6 +7721,11 @@
"@types/react": "*"
}
},
"@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
},
"@types/shallowequal": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz",
@@ -8403,7 +8448,6 @@
"graceful-fs": "^4.1.11",
"lru-cache": "^4.1.1",
"mississippi": "^2.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.2",
@@ -9342,6 +9386,15 @@
"pkg-dir": "^3.0.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@@ -9519,6 +9572,11 @@
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
"integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
},
"base64-arraybuffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
},
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@@ -10075,9 +10133,9 @@
"dev": true
},
"canvas": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.0.tgz",
"integrity": "sha512-bEO9f1ThmbknLPxCa8Es7obPlN9W3stB1bo7njlhOFKIdUTldeTqXCh9YclCPAi2pSQs84XA0jq/QEZXSzgyMw==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
"integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
"requires": {
"nan": "^2.14.0",
"node-pre-gyp": "^0.11.0",
@@ -10656,6 +10714,14 @@
"run-queue": "^1.0.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -10874,6 +10940,14 @@
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.1.1.tgz",
"integrity": "sha512-/PX6Bkk77ShgbOx/mpawHdEvS3PGgy1mmMktcztDPndWdMJxcorcQiivrs+nEljqtBpvNEhAmQky9tQR6FSm8Q=="
},
"css-line-break": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
"requires": {
"base64-arraybuffer": "^0.2.0"
}
},
"css-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz",
@@ -12744,6 +12818,12 @@
"integrity": "sha1-EjaoEjkTkKGHetQAfCbnRTQclR8=",
"dev": true
},
"eslint-plugin-prefer-arrow": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.2.tgz",
"integrity": "sha512-C8YMhL+r8RMeMdYAw/rQtE6xNdMulj+zGWud/qIGnlmomiPRaLDGLMeskZ3alN6uMBojmooRimtdrXebLN4svQ==",
"dev": true
},
"eslint-plugin-react": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.0.tgz",
@@ -15557,6 +15637,14 @@
}
}
},
"html2canvas": {
"version": "1.0.0-rc.5",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.5.tgz",
"integrity": "sha512-DtNqPxJNXPoTajs+lVQzGS1SULRI4GQaROeU5R41xH8acffHukxRh/NBVcTBsfCkJSkLq91rih5TpbEwUP9yWA==",
"requires": {
"css-line-break": "1.1.1"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -19754,6 +19842,16 @@
"mkdirp": "^0.5.1",
"slash": "^2.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"jest-validate": {
@@ -20107,6 +20205,11 @@
"esprima": "^4.0.0"
}
},
"jsbi": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz",
"integrity": "sha512-nBJqA0C6Qns+ZxurbEoIR56wyjiUszpNy70FHvxO5ervMoCbZVE3z3kxr5nKGhlxr/9MhKTSUBs7cAwwuf3g9w=="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
@@ -20337,6 +20440,16 @@
"dev": true,
"optional": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
},
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@@ -21287,12 +21400,9 @@
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"mkdirp-classic": {
"version": "0.5.3",
@@ -21338,6 +21448,14 @@
"run-queue": "^1.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -21422,9 +21540,9 @@
}
},
"needle": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz",
"integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
@@ -21677,6 +21795,14 @@
"tar": "^4"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -22171,11 +22297,11 @@
"integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo="
},
"p-retry": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
"integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
"dev": true,
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz",
"integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==",
"requires": {
"@types/retry": "^0.12.0",
"retry": "^0.12.0"
}
},
@@ -22576,6 +22702,15 @@
"requires": {
"ms": "^2.1.1"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
}
}
},
@@ -23944,8 +24079,7 @@
"retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
"dev": true
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
},
"reusify": {
"version": "1.0.4",
@@ -24502,9 +24636,9 @@
"integrity": "sha1-ZfDBWZNSs1Ny7KrlolDmEHN27Wk="
},
"simple-concat": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
},
"simple-get": {
"version": "3.1.0",
@@ -25476,6 +25610,16 @@
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"tar-fs": {
@@ -25833,6 +25977,14 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"yargs-parser": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
@@ -26901,6 +27053,15 @@
"readable-stream": "^2.0.1"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@@ -27012,6 +27173,15 @@
"integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==",
"dev": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
@@ -27224,6 +27394,15 @@
"readable-stream": "^2.0.1"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@@ -27303,6 +27482,15 @@
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"p-retry": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
"integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
"dev": true,
"requires": {
"retry": "^0.12.0"
}
},
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
@@ -27531,6 +27719,17 @@
"dev": true,
"requires": {
"mkdirp": "^0.5.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"write-file-atomic": {

View File

@@ -4,7 +4,7 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/cosmos": "3.7.4",
"@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1",
@@ -34,13 +34,15 @@
"@nteract/transform-vega": "7.0.6",
"@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110",
"@uifabric/styling": "7.13.7",
"abort-controller": "3.0.0",
"applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0",
"bootstrap": "3.4.1",
"canvas": "2.6.0",
"canvas": "2.6.1",
"clean-webpack-plugin": "0.1.19",
"copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2",
@@ -54,15 +56,18 @@
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2",
"hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5",
"immutable": "4.0.0-rc.12",
"is-ci": "2.0.0",
"jquery": "3.5.1",
"jquery-typeahead": "2.10.6",
"jquery-ui-dist": "1.12.1",
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.15.6",
"object.entries": "1.1.0",
"office-ui-fabric-react": "7.121.10",
"p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3",
"promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0",
@@ -130,6 +135,7 @@
"eslint": "7.3.1",
"eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react": "7.20.0",
"expose-loader": "0.7.5",
"file-loader": "2.0.0",
@@ -147,6 +153,7 @@
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.0",
"prettier": "1.19.1",
"puppeteer": "4.0.0",
"raw-loader": "0.5.1",
@@ -187,7 +194,8 @@
"build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
"autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks"
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
},
"repository": {
"type": "git",

View File

@@ -1,13 +0,0 @@
import * as ViewModels from "../Contracts/ViewModels";
export class DefaultApi implements ViewModels.CosmosDbApi {
public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return false;
};
}
export class CassandraApi implements ViewModels.CosmosDbApi {
public isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system";
};
}

View File

@@ -1,5 +1,5 @@
import { AutopilotTier } from "../Contracts/DataModels";
import { config } from "../Config";
import { configContext } from "../ConfigContext";
import { HashMap } from "./HashMap";
export class AuthorizationEndpoints {
@@ -7,14 +7,23 @@ export class AuthorizationEndpoints {
public static common: string = "https://login.windows.net/";
}
export class CodeOfConductEndpoints {
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
}
export class BackendEndpoints {
public static localhost: string = "https://localhost:12900";
public static dev: string = "https://ext.documents-dev.windows-int.net";
public static productionPortal: string = config.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
public static productionPortal: string = configContext.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
}
export class EndpointsRegex {
public static readonly cassandra = "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com";
public static readonly cassandra = [
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
"HostName=(.*).cassandra.cosmos.azure.com"
];
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
@@ -101,6 +110,7 @@ export class CapabilityNames {
public static readonly EnableNotebooks: string = "EnableNotebooks";
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless";
}
export class Features {
@@ -112,6 +122,8 @@ export class Features {
public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableCodeOfConduct = "enablecodeofconduct";
public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl";
@@ -122,6 +134,7 @@ export class Features {
public static readonly enableAutoPilotV2 = "enableautopilotv2";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSDKoperations = "enablesdkoperations";
}
export class AfecFeatures {
@@ -351,6 +364,7 @@ export class HttpStatusCodes {
public static readonly Created: number = 201;
public static readonly Accepted: number = 202;
public static readonly NoContent: number = 204;
public static readonly NotModified: number = 304;
public static readonly Unauthorized: number = 401;
public static readonly Forbidden: number = 403;
public static readonly NotFound: number = 404;

View File

@@ -1,6 +1,7 @@
import { CosmosClient, tokenProvider, endpoint, requestPlugin, getTokenFromAuthService } from "./CosmosClient";
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
import { config, Platform } from "../Config";
import { configContext, Platform, updateConfigContext, resetConfigContext } from "../ConfigContext";
import { updateUserContext } from "../UserContext";
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
describe("tokenProvider", () => {
const options = {
@@ -32,7 +33,9 @@ describe("tokenProvider", () => {
});
it("does not call the auth service if a master key is set", async () => {
CosmosClient.masterKey("foo");
updateUserContext({
masterKey: "foo"
});
await tokenProvider(options);
expect((window.fetch as any).mock.calls.length).toBe(0);
});
@@ -41,7 +44,7 @@ describe("tokenProvider", () => {
describe("getTokenFromAuthService", () => {
beforeEach(() => {
delete window.dataExplorer;
delete config.BACKEND_ENDPOINT;
resetConfigContext();
window.fetch = jest.fn().mockImplementation(() => {
return {
json: () => "{}",
@@ -64,7 +67,9 @@ describe("getTokenFromAuthService", () => {
});
it("builds the correct URL in dev", () => {
config.BACKEND_ENDPOINT = "https://localhost:1234";
updateConfigContext({
BACKEND_ENDPOINT: "https://localhost:1234"
});
getTokenFromAuthService("GET", "dbs", "foo");
expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
@@ -75,24 +80,28 @@ describe("getTokenFromAuthService", () => {
describe("endpoint", () => {
it("falls back to _databaseAccount", () => {
CosmosClient.databaseAccount({
id: "foo",
name: "foo",
location: "foo",
type: "foo",
kind: "foo",
tags: [],
properties: {
documentEndpoint: "bar",
gremlinEndpoint: "foo",
tableEndpoint: "foo",
cassandraEndpoint: "foo"
updateUserContext({
databaseAccount: {
id: "foo",
name: "foo",
location: "foo",
type: "foo",
kind: "foo",
tags: [],
properties: {
documentEndpoint: "bar",
gremlinEndpoint: "foo",
tableEndpoint: "foo",
cassandraEndpoint: "foo"
}
}
});
expect(endpoint()).toEqual("bar");
});
it("uses _endpoint if set", () => {
CosmosClient.endpoint("baz");
updateUserContext({
endpoint: "baz"
});
expect(endpoint()).toEqual("baz");
});
});
@@ -100,17 +109,17 @@ describe("endpoint", () => {
describe("requestPlugin", () => {
beforeEach(() => {
delete window.dataExplorerPlatform;
delete config.PROXY_PATH;
delete config.BACKEND_ENDPOINT;
delete config.PROXY_PATH;
resetConfigContext();
});
describe("Hosted", () => {
it("builds a proxy URL in development", () => {
const next = jest.fn();
config.platform = Platform.Hosted;
config.BACKEND_ENDPOINT = "https://localhost:1234";
config.PROXY_PATH = "/proxy";
updateConfigContext({
platform: Platform.Hosted,
BACKEND_ENDPOINT: "https://localhost:1234",
PROXY_PATH: "/proxy"
});
const headers = {};
const endpoint = "https://docs.azure.com";
const path = "/dbs/foo";
@@ -122,8 +131,7 @@ describe("requestPlugin", () => {
describe("Emulator", () => {
it("builds a url for emulator proxy via webpack", () => {
const next = jest.fn();
config.platform = Platform.Emulator;
config.PROXY_PATH = "/proxy";
updateConfigContext({ platform: Platform.Emulator, PROXY_PATH: "/proxy" });
const headers = {};
const endpoint = "";
const path = "/dbs/foo";

View File

@@ -1,39 +1,28 @@
import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { DatabaseAccount } from "../Contracts/DataModels";
import { HttpHeaders, EmulatorMasterKey } from "./Constants";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { config, Platform } from "../Config";
let _client: Cosmos.CosmosClient;
let _masterKey: string;
let _endpoint: string;
let _authorizationToken: string;
let _accessToken: string;
let _databaseAccount: DatabaseAccount;
let _subscriptionId: string;
let _resourceGroup: string;
let _resourceToken: string;
import { configContext, Platform } from "../ConfigContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { userContext } from "../UserContext";
const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo;
if (config.platform === Platform.Emulator) {
if (configContext.platform === Platform.Emulator) {
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
}
if (_masterKey) {
if (userContext.masterKey) {
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
}
if (_resourceToken) {
return _resourceToken;
if (userContext.resourceToken) {
return userContext.resourceToken;
}
const result = await getTokenFromAuthService(verb, resourceType, resourceId);
@@ -42,28 +31,33 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
};
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
requestContext.endpoint = config.PROXY_PATH;
requestContext.endpoint = configContext.PROXY_PATH;
requestContext.headers["x-ms-proxy-target"] = endpoint();
return next(requestContext);
};
export const endpoint = () => {
if (config.platform === Platform.Emulator) {
if (configContext.platform === Platform.Emulator) {
// In worker scope, _global(self).parent does not exist
const location = _global.parent ? _global.parent.location : _global.location;
return config.EMULATOR_ENDPOINT || location.origin;
return configContext.EMULATOR_ENDPOINT || location.origin;
}
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
return (
userContext.endpoint ||
(userContext.databaseAccount &&
userContext.databaseAccount.properties &&
userContext.databaseAccount.properties.documentEndpoint)
);
};
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
try {
const host = config.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint();
const host = configContext.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint();
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-encrypted-auth-token": _accessToken
"x-ms-encrypted-auth-token": userContext.accessToken
},
body: JSON.stringify({
verb,
@@ -75,106 +69,25 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
const result = JSON.parse(await response.json());
return result;
} catch (error) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`
);
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`);
return Promise.reject(error);
}
}
export const CosmosClient = {
client(): Cosmos.CosmosClient {
if (_client) {
return _client;
}
const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || " ", // CosmosClient gets upset if we pass a falsy value here
key: _masterKey,
tokenProvider,
connectionPolicy: {
enableEndpointDiscovery: false
},
userAgentSuffix: "Azure Portal"
};
export function client(): Cosmos.CosmosClient {
const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || " ", // CosmosClient gets upset if we pass a falsy value here
key: userContext.masterKey,
tokenProvider,
connectionPolicy: {
enableEndpointDiscovery: false
},
userAgentSuffix: "Azure Portal"
};
// In development we proxy requests to the backend via webpack. This is removed in production bundles.
if (process.env.NODE_ENV === "development") {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
}
_client = new Cosmos.CosmosClient(options);
return _client;
},
authorizationToken(value?: string): string {
if (typeof value === "undefined") {
return _authorizationToken;
}
_authorizationToken = value;
_client = null;
return value;
},
accessToken(value?: string): string {
if (typeof value === "undefined") {
return _accessToken;
}
_accessToken = value;
_client = null;
return value;
},
masterKey(value?: string): string {
if (typeof value === "undefined") {
return _masterKey;
}
_client = null;
_masterKey = value;
return value;
},
endpoint(value?: string): string {
if (typeof value === "undefined") {
return _endpoint;
}
_client = null;
_endpoint = value;
return value;
},
databaseAccount(value?: DatabaseAccount): DatabaseAccount {
if (typeof value === "undefined") {
return _databaseAccount || ({} as any);
}
_client = null;
_databaseAccount = value;
return value;
},
subscriptionId(value?: string): string {
if (typeof value === "undefined") {
return _subscriptionId;
}
_client = null;
_subscriptionId = value;
return value;
},
resourceGroup(value?: string): string {
if (typeof value === "undefined") {
return _resourceGroup;
}
_client = null;
_resourceGroup = value;
return value;
},
resourceToken(value?: string): string {
if (typeof value === "undefined") {
return _resourceToken;
}
_client = null;
_resourceToken = value;
return value;
// In development we proxy requests to the backend via webpack. This is removed in production bundles.
if (process.env.NODE_ENV === "development") {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
}
};
return new Cosmos.CosmosClient(options);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import { AuthType } from "../AuthType";
import { StringUtils } from "../Utils/StringUtils";
import Explorer from "../Explorer/Explorer";
export default class EnvironmentUtility {
public static getMongoBackendEndpoint(serverId: string, location: string, extensionEndpoint: string = ""): string {
@@ -26,7 +27,7 @@ export default class EnvironmentUtility {
return window.authType === AuthType.AAD;
}
public static getCassandraBackendEndpoint(explorer: ViewModels.Explorer): string {
public static getCassandraBackendEndpoint(explorer: Explorer): string {
const defaultLocation: string = "default";
const location: string = EnvironmentUtility.normalizeRegionName(explorer.databaseAccount().location);
return (

View File

@@ -1,4 +1,4 @@
import * as DataModels from "../Contracts/DataModels";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
export function replaceKnownError(err: string): string {
@@ -7,6 +7,8 @@ export function replaceKnownError(err: string): string {
err.indexOf("SharedOffer is Disabled for your account") >= 0
) {
return "Database throughput is not supported for internal subscriptions.";
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
}
return err;

View File

@@ -1,46 +1,26 @@
jest.mock("./MessageHandler");
import { LogEntryLevel } from "../Contracts/Diagnostics";
import * as Logger from "./Logger";
import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { sendMessage } from "./MessageHandler";
describe("Logger", () => {
let sendMessageSpy: jasmine.Spy;
beforeEach(() => {
sendMessageSpy = spyOn(MessageHandler, "sendMessage");
});
afterEach(() => {
sendMessageSpy = null;
jest.resetAllMocks();
});
it("should log info messages", () => {
Logger.logInfo("Test info", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Verbose);
expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test info");
expect(sendMessage).toBeCalled();
});
it("should log error messages", () => {
Logger.logError("Test error", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Error);
expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test error");
expect(sendMessage).toBeCalled();
});
it("should log warnings", () => {
Logger.logWarning("Test warning", "DocDB");
const spyArgs = sendMessageSpy.calls.mostRecent().args[0];
expect(spyArgs.type).toBe(MessageTypes.LogInfo);
expect(spyArgs.data).toContain(LogEntryLevel.Warning);
expect(spyArgs.data).toContain("DocDB");
expect(spyArgs.data).toContain("Test warning");
expect(sendMessage).toBeCalled();
});
});

View File

@@ -1,4 +1,4 @@
import { MessageHandler } from "./MessageHandler";
import { sendMessage } from "./MessageHandler";
import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts";
import { appInsights } from "../Shared/appInsights";
import { SeverityLevel } from "@microsoft/applicationinsights-web";
@@ -33,7 +33,7 @@ export function logError(message: string | Error, area: string, code?: number):
}
function _logEntry(entry: Diagnostics.LogEntry): void {
MessageHandler.sendMessage({
sendMessage({
type: MessageTypes.LogInfo,
data: JSON.stringify(entry)
});

View File

@@ -1,65 +1,29 @@
import Q from "q";
import { CachedDataPromise, MessageHandler } from "./MessageHandler";
import * as MessageHandler from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
class MockMessageHandler extends MessageHandler {
public static addToMap(key: string, value: CachedDataPromise<any>): void {
MessageHandler.RequestMap[key] = value;
}
public static mapContainsKey(key: string): boolean {
return MessageHandler.RequestMap[key] != null;
}
public static clearAllEntries(): void {
MessageHandler.RequestMap = {};
}
public static runGarbageCollector(): void {
MessageHandler.runGarbageCollector();
}
}
describe("Message Handler", () => {
beforeEach(() => {
MockMessageHandler.clearAllEntries();
});
xit("should send cached data message", (done: any) => {
const testValidationCallback = (e: MessageEvent) => {
expect(e.data.data).toEqual(
jasmine.objectContaining({ type: MessageTypes.AllDatabases, params: ["some param"] })
);
e.currentTarget.removeEventListener(e.type, testValidationCallback);
done();
};
window.parent.addEventListener("message", testValidationCallback);
MockMessageHandler.sendCachedDataMessage(MessageTypes.AllDatabases, ["some param"]);
});
it("should handle cached message", () => {
let mockPromise: CachedDataPromise<any> = {
it("should handle cached message", async () => {
let mockPromise = {
id: "123",
startTime: new Date(),
deferred: Q.defer<any>()
};
let mockMessage = { message: { id: "123", data: "{}" } };
MockMessageHandler.addToMap(mockPromise.id, mockPromise);
MockMessageHandler.handleCachedDataMessage(mockMessage);
MessageHandler.RequestMap[mockPromise.id] = mockPromise;
MessageHandler.handleCachedDataMessage(mockMessage);
expect(mockPromise.deferred.promise.isFulfilled()).toBe(true);
});
it("should delete fulfilled promises on running the garbage collector", () => {
let mockPromise: CachedDataPromise<any> = {
it("should delete fulfilled promises on running the garbage collector", async () => {
let message = {
id: "123",
startTime: new Date(),
deferred: Q.defer<any>()
};
MockMessageHandler.addToMap(mockPromise.id, mockPromise);
mockPromise.deferred.reject("some error");
MockMessageHandler.runGarbageCollector();
expect(MockMessageHandler.mapContainsKey(mockPromise.id)).toBe(false);
MessageHandler.handleCachedDataMessage(message);
MessageHandler.runGarbageCollector();
expect(MessageHandler.RequestMap["123"]).toBeUndefined();
});
});

View File

@@ -1,85 +1,73 @@
import { MessageTypes } from "../Contracts/ExplorerContracts";
import Q from "q";
import * as _ from "underscore";
import * as Constants from "./Constants";
export interface CachedDataPromise<T> {
deferred: Q.Deferred<T>;
startTime: Date;
id: string;
}
/**
* For some reason, typescript emits a Map() in the compiled js output(despite the target being set to ES5) forcing us to define our own polyfill,
* so we define our own custom implementation of the ES6 Map to work around it.
*/
type Map = { [key: string]: CachedDataPromise<any> };
export class MessageHandler {
protected static RequestMap: Map = {};
public static handleCachedDataMessage(message: any): void {
const messageContent = message && message.message;
if (
message == null ||
messageContent == null ||
messageContent.id == null ||
!MessageHandler.RequestMap[messageContent.id]
) {
return;
}
const cachedDataPromise = MessageHandler.RequestMap[messageContent.id];
if (messageContent.error != null) {
cachedDataPromise.deferred.reject(messageContent.error);
} else {
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
}
MessageHandler.runGarbageCollector();
}
public static sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes,
params: Object[],
timeoutInMs?: number
): Q.Promise<TResponseDataModel> {
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
deferred: Q.defer<TResponseDataModel>(),
startTime: new Date(),
id: _.uniqueId()
};
MessageHandler.RequestMap[cachedDataPromise.id] = cachedDataPromise;
MessageHandler.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
//TODO: Use telemetry to measure optimal time to resolve/reject promises
return cachedDataPromise.deferred.promise.timeout(
timeoutInMs || Constants.ClientDefaults.requestTimeoutMs,
"Timed out while waiting for response from portal"
);
}
public static sendMessage(data: any): void {
if (MessageHandler.canSendMessage()) {
window.parent.postMessage(
{
signature: "pcIframe",
data: data
},
window.document.referrer
);
}
}
public static canSendMessage(): boolean {
return window.parent !== window;
}
protected static runGarbageCollector() {
Object.keys(MessageHandler.RequestMap).forEach((key: string) => {
const promise: Q.Promise<any> = MessageHandler.RequestMap[key].deferred.promise;
if (promise.isFulfilled() || promise.isRejected()) {
delete MessageHandler.RequestMap[key];
}
});
}
}
import { MessageTypes } from "../Contracts/ExplorerContracts";
import Q from "q";
import * as _ from "underscore";
import * as Constants from "./Constants";
export interface CachedDataPromise<T> {
deferred: Q.Deferred<T>;
startTime: Date;
id: string;
}
export const RequestMap: Record<string, CachedDataPromise<any>> = {};
export function handleCachedDataMessage(message: any): void {
const messageContent = message && message.message;
if (message == null || messageContent == null || messageContent.id == null || !RequestMap[messageContent.id]) {
return;
}
const cachedDataPromise = RequestMap[messageContent.id];
if (messageContent.error != null) {
cachedDataPromise.deferred.reject(messageContent.error);
} else {
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
}
runGarbageCollector();
}
export function sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes,
params: Object[],
timeoutInMs?: number
): Q.Promise<TResponseDataModel> {
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
deferred: Q.defer<TResponseDataModel>(),
startTime: new Date(),
id: _.uniqueId()
};
RequestMap[cachedDataPromise.id] = cachedDataPromise;
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
//TODO: Use telemetry to measure optimal time to resolve/reject promises
return cachedDataPromise.deferred.promise.timeout(
timeoutInMs || Constants.ClientDefaults.requestTimeoutMs,
"Timed out while waiting for response from portal"
);
}
export function sendMessage(data: any): void {
if (canSendMessage()) {
window.parent.postMessage(
{
signature: "pcIframe",
data: data
},
window.document.referrer
);
}
}
export function canSendMessage(): boolean {
return window.parent !== window;
}
// TODO: This is exported just for testing. It should not be.
export function runGarbageCollector() {
Object.keys(RequestMap).forEach((key: string) => {
const promise: Q.Promise<any> = RequestMap[key].deferred.promise;
if (promise.isFulfilled() || promise.isRejected()) {
delete RequestMap[key];
}
});
}

View File

@@ -1,16 +1,18 @@
import { AuthType } from "../AuthType";
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { updateUserContext } from "../UserContext";
import {
_createMongoCollectionWithARM,
deleteDocument,
getEndpoint,
queryDocuments,
readDocument,
updateDocument
updateDocument,
_createMongoCollectionWithARM
} from "./MongoProxyClient";
import { AuthType } from "../AuthType";
import { Collection, DatabaseAccount, DocumentId } from "../Contracts/ViewModels";
import { config } from "../Config";
import { CosmosClient } from "./CosmosClient";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
const databaseId = "testDB";
@@ -60,13 +62,15 @@ const databaseAccount = {
tableEndpoint: "foo",
cassandraEndpoint: "foo"
}
};
} as DatabaseAccount;
describe("MongoProxyClient", () => {
describe("queryDocuments", () => {
beforeEach(() => {
delete config.BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
resetConfigContext();
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -86,7 +90,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
queryDocuments(databaseId, collection, true, "{}");
expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -96,8 +100,10 @@ describe("MongoProxyClient", () => {
});
describe("readDocument", () => {
beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
resetConfigContext();
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -117,7 +123,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -127,8 +133,10 @@ describe("MongoProxyClient", () => {
});
describe("createDocument", () => {
beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
resetConfigContext();
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -148,7 +156,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -158,8 +166,10 @@ describe("MongoProxyClient", () => {
});
describe("updateDocument", () => {
beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
resetConfigContext();
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -171,7 +181,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct URL", () => {
updateDocument(databaseId, collection, documentId, {});
updateDocument(databaseId, collection, documentId, "{}");
expect(window.fetch).toHaveBeenCalledWith(
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object)
@@ -179,8 +189,8 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
updateDocument(databaseId, collection, documentId, {});
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
updateDocument(databaseId, collection, documentId, "{}");
expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object)
@@ -189,8 +199,10 @@ describe("MongoProxyClient", () => {
});
describe("deleteDocument", () => {
beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
resetConfigContext();
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -210,7 +222,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
deleteDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
@@ -220,9 +232,11 @@ describe("MongoProxyClient", () => {
});
describe("getEndpoint", () => {
beforeEach(() => {
delete config.MONGO_BACKEND_ENDPOINT;
resetConfigContext();
delete window.authType;
CosmosClient.databaseAccount(databaseAccount as any);
updateUserContext({
databaseAccount
});
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -235,7 +249,7 @@ describe("MongoProxyClient", () => {
});
it("returns a development endpoint", () => {
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
});

View File

@@ -1,21 +1,22 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import queryString from "querystring";
import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import * as DataExplorerConstants from "../Common/Constants";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import EnvironmentUtility from "./EnvironmentUtility";
import queryString from "querystring";
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { AuthType } from "../AuthType";
import { Collection } from "../Contracts/ViewModels";
import { config } from "../Config";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { CosmosClient } from "./CosmosClient";
import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
import { Collection } from "../Contracts/ViewModels";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { userContext } from "../UserContext";
import EnvironmentUtility from "./EnvironmentUtility";
import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler";
const defaultHeaders = {
[HttpHeaders.apiType]: ApiType.MongoDB.toString(),
@@ -23,29 +24,29 @@ const defaultHeaders = {
[CosmosSDKConstants.HttpHeaders.Version]: "2017-11-15"
};
function authHeaders(): any {
function authHeaders() {
if (window.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: CosmosClient.accessToken() };
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
} else {
return { [HttpHeaders.authorization]: CosmosClient.authorizationToken() };
return { [HttpHeaders.authorization]: userContext.authorizationToken };
}
}
export function queryIterator(databaseId: string, collection: Collection, query: string): any {
export function queryIterator(databaseId: string, collection: Collection, query: string): MinimalQueryIterator {
let continuationToken: string;
return {
fetchNext: () => {
return queryDocuments(databaseId, collection, false, query).then(response => {
continuationToken = response.continuationToken;
const headers = {} as any;
response.headers.forEach((value: any, key: any) => {
const headers: { [key: string]: string | number } = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
return {
resources: response.documents,
headers,
requestCharge: headers[CosmosSDKConstants.HttpHeaders.RequestCharge],
activityId: headers[CosmosSDKConstants.HttpHeaders.ActivityId],
requestCharge: Number(headers[CosmosSDKConstants.HttpHeaders.RequestCharge]),
activityId: String(headers[CosmosSDKConstants.HttpHeaders.ActivityId]),
hasMoreResults: !!continuationToken
};
});
@@ -66,7 +67,7 @@ export function queryDocuments(
query: string,
continuationToken?: string
): Promise<QueryResponse> {
const databaseAccount = CosmosClient.databaseAccount();
const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = {
db: databaseId,
@@ -74,8 +75,8 @@ export function queryDocuments(
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
rid: collection.rid,
rtype: "docs",
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
pk:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
@@ -114,16 +115,17 @@ export function queryDocuments(
headers: response.headers
};
}
return errorHandling(response, "querying documents", params);
errorHandling(response, "querying documents", params);
return undefined;
});
}
export function readDocument(
databaseId: string,
collection: Collection,
documentId: ViewModels.DocumentId
documentId: DocumentId
): Promise<DataModels.DocumentId> {
const databaseAccount = CosmosClient.databaseAccount();
const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 4).join("/");
@@ -134,8 +136,8 @@ export function readDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid,
rtype: "docs",
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -165,9 +167,9 @@ export function createDocument(
databaseId: string,
collection: Collection,
partitionKeyProperty: string,
documentContent: any
documentContent: unknown
): Promise<DataModels.DocumentId> {
const databaseAccount = CosmosClient.databaseAccount();
const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = {
db: databaseId,
@@ -175,8 +177,8 @@ export function createDocument(
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
rid: collection.rid,
rtype: "docs",
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
};
@@ -203,10 +205,10 @@ export function createDocument(
export function updateDocument(
databaseId: string,
collection: Collection,
documentId: ViewModels.DocumentId,
documentContent: any
documentId: DocumentId,
documentContent: string
): Promise<DataModels.DocumentId> {
const databaseAccount = CosmosClient.databaseAccount();
const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/");
@@ -217,8 +219,8 @@ export function updateDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid,
rtype: "docs",
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -244,12 +246,8 @@ export function updateDocument(
});
}
export function deleteDocument(
databaseId: string,
collection: Collection,
documentId: ViewModels.DocumentId
): Promise<any> {
const databaseAccount = CosmosClient.databaseAccount();
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
const databaseAccount = userContext.databaseAccount;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/");
@@ -260,8 +258,8 @@ export function deleteDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid,
rtype: "docs",
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -295,8 +293,8 @@ export function createMongoCollectionWithProxy(
sharedThroughput: boolean,
isSharded: boolean,
autopilotOptions?: DataModels.RpOptions
): Promise<any> {
const databaseAccount = CosmosClient.databaseAccount();
): Promise<DataModels.Collection> {
const databaseAccount = userContext.databaseAccount;
const params: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: databaseId,
@@ -308,8 +306,8 @@ export function createMongoCollectionWithProxy(
is: isSharded,
rid: "",
rtype: "colls",
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
isAutoPilot: false
};
@@ -335,7 +333,7 @@ export function createMongoCollectionWithProxy(
)
.then(response => {
if (response.ok) {
return undefined;
return response.json();
}
return errorHandling(response, "creating collection", params);
});
@@ -352,8 +350,8 @@ export function createMongoCollectionWithARM(
sharedThroughput: boolean,
isSharded: boolean,
additionalOptions?: DataModels.RpOptions
): Promise<any> {
const databaseAccount = CosmosClient.databaseAccount();
): Promise<DataModels.CreateCollectionWithRpResponse> {
const databaseAccount = userContext.databaseAccount;
const params: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: databaseId,
@@ -365,8 +363,8 @@ export function createMongoCollectionWithARM(
is: isSharded,
rid: "",
rtype: "colls",
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
analyticalStorageTtl
};
@@ -383,11 +381,11 @@ export function createMongoCollectionWithARM(
return _createMongoCollectionWithARM(armEndpoint, params, additionalOptions);
}
export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string {
export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
const serverId = window.dataExplorer.serverId();
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
let url = config.MONGO_BACKEND_ENDPOINT
? config.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
let url = configContext.MONGO_BACKEND_ENDPOINT
? configContext.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
if (window.authType === AuthType.EncryptedToken) {
@@ -396,7 +394,9 @@ export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string
return url;
}
async function errorHandling(response: any, action: string, params: any): Promise<any> {
// TODO: This function throws most of the time except on Forbidden which is a bit strange
// It causes problems for TypeScript understanding the types
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
const errorMessage = await response.text();
// Log the error where the user can see it
NotificationConsoleUtils.logConsoleMessage(
@@ -404,23 +404,21 @@ async function errorHandling(response: any, action: string, params: any): Promis
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
);
if (response.status === HttpStatusCodes.Forbidden) {
MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
return;
}
throw new Error(errorMessage);
}
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${
CosmosClient.databaseAccount().name
}/mongodbDatabases/${params.db}/collections/${params.coll}`;
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
}
export async function _createMongoCollectionWithARM(
armEndpoint: string,
params: DataModels.MongoParameters,
rpOptions: DataModels.RpOptions
): Promise<any> {
): Promise<DataModels.CreateCollectionWithRpResponse> {
const rpPayloadToCreateCollection: DataModels.MongoCreationRequest = {
properties: {
resource: {
@@ -448,12 +446,13 @@ export async function _createMongoCollectionWithARM(
}
try {
await new ResourceProviderClient(armEndpoint).putAsync(
return new ResourceProviderClient<DataModels.CreateCollectionWithRpResponse>(armEndpoint).putAsync(
getARMCreateCollectionEndpoint(params),
DataExplorerConstants.ArmApiVersions.publicVersion,
rpPayloadToCreateCollection
);
} catch (response) {
return errorHandling(response, "creating collection", undefined);
errorHandling(response, "creating collection", undefined);
return undefined;
}
}

View File

@@ -2,11 +2,10 @@ import "jquery";
import * as Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { CosmosClient } from "./CosmosClient";
import { userContext } from "../UserContext";
export class NotificationsClientBase implements ViewModels.NotificationsClient {
export class NotificationsClientBase {
private _extensionEndpoint: string;
private _notificationsApiSuffix: string;
@@ -16,10 +15,10 @@ export class NotificationsClientBase implements ViewModels.NotificationsClient {
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
const subscriptionId: string = CosmosClient.subscriptionId();
const resourceGroup: string = CosmosClient.resourceGroup();
const url: string = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const databaseAccount = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const url = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers: any = {};
headers[authorizationHeader.header] = authorizationHeader.token;

View File

@@ -1,17 +1,26 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import * as ErrorParserUtility from "./ErrorParserUtility";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "./CosmosClient";
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as Logger from "./Logger";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext";
import {
createDocument,
deleteDocument,
getOrCreateDatabaseAndCollection,
queryDocuments,
queryDocumentsPage
} from "./DocumentClientUtilityBase";
import * as ErrorParserUtility from "./ErrorParserUtility";
import * as Logger from "./Logger";
export class QueriesClient implements ViewModels.QueriesClient {
export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = {
paths: [`/${SavedQueries.PartitionKeyProperty}`],
kind: BackendDefaults.partitionKeyKind,
@@ -20,7 +29,7 @@ export class QueriesClient implements ViewModels.QueriesClient {
private static readonly FetchQuery: string = "SELECT * FROM c";
private static readonly FetchMongoQuery: string = "{}";
public constructor(private container: ViewModels.Explorer) {}
public constructor(private container: Explorer) {}
public async setupQueriesCollection(): Promise<DataModels.Collection> {
const queriesCollection: ViewModels.Collection = this.findQueriesCollection();
@@ -32,14 +41,13 @@ export class QueriesClient implements ViewModels.QueriesClient {
ConsoleDataType.InProgress,
"Setting up account for saving queries"
);
return this.container.documentClientUtility
.getOrCreateDatabaseAndCollection({
collectionId: SavedQueries.CollectionName,
databaseId: SavedQueries.DatabaseName,
partitionKey: QueriesClient.PartitionKey,
offerThroughput: SavedQueries.OfferThroughput,
databaseLevelThroughput: undefined
})
return getOrCreateDatabaseAndCollection({
collectionId: SavedQueries.CollectionName,
databaseId: SavedQueries.DatabaseName,
partitionKey: QueriesClient.PartitionKey,
offerThroughput: SavedQueries.OfferThroughput,
databaseLevelThroughput: undefined
})
.then(
(collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -88,8 +96,7 @@ export class QueriesClient implements ViewModels.QueriesClient {
`Saving query ${query.queryName}`
);
query.id = query.queryName;
return this.container.documentClientUtility
.createDocument(queriesCollection, query)
return createDocument(queriesCollection, query)
.then(
(savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -130,17 +137,11 @@ export class QueriesClient implements ViewModels.QueriesClient {
const options: any = { enableCrossPartitionQuery: true };
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
return this.container.documentClientUtility
.queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
.then(
(queryIterator: QueryIterator<ItemDefinition & Resource>) => {
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
this.container.documentClientUtility.queryDocumentsPage(
queriesCollection.id(),
queryIterator,
firstItemIndex,
options
);
queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
return QueryUtils.queryAllPages(fetchQueries).then(
(results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
@@ -216,17 +217,16 @@ export class QueriesClient implements ViewModels.QueriesClient {
`Deleting query ${query.queryName}`
);
query.id = query.queryName;
const documentId: ViewModels.DocumentId = new DocumentId(
const documentId = new DocumentId(
{
partitionKey: QueriesClient.PartitionKey,
partitionKeyProperty: "id"
} as ViewModels.DocumentsTab,
} as DocumentsTab,
query,
query.queryName
); // TODO: Remove DocumentId's dependency on DocumentsTab
const options: any = { partitionKey: query.resourceId };
return this.container.documentClientUtility
.deleteDocument(queriesCollection, documentId)
return deleteDocument(queriesCollection, documentId)
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
@@ -249,10 +249,10 @@ export class QueriesClient implements ViewModels.QueriesClient {
}
public getResourceId(): string {
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
const databaseAccountName: string = (databaseAccount && databaseAccount.name) || "";
const subscriptionId: string = CosmosClient.subscriptionId() || "";
const resourceGroup: string = CosmosClient.resourceGroup() || "";
const databaseAccount = userContext.databaseAccount;
const databaseAccountName = (databaseAccount && databaseAccount.name) || "";
const subscriptionId = userContext.subscriptionId || "";
const resourceGroup = userContext.resourceGroup || "";
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`;
}

View File

@@ -0,0 +1,250 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DatabaseResponse } from "@azure/cosmos";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import {
SqlDatabaseCreateUpdateParameters,
CreateUpdateOptions
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import {
createUpdateCassandraKeyspace,
getCassandraKeyspace
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateMongoDBDatabase,
getMongoDBDatabase
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import {
createUpdateGremlinDatabase,
getGremlinDatabase
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
let database: DataModels.Database;
const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`);
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
database = await createDatabaseWithARM(params);
} else {
database = await createDatabaseWithSDK(params);
}
} catch (error) {
logConsoleError(`Error while creating database ${params.databaseId}:\n ${error.message}`);
logError(JSON.stringify(error), "CreateDatabase", error.code);
sendNotificationForError(error);
clearMessage();
throw error;
}
logConsoleInfo(`Successfully created database ${params.databaseId}`);
await refreshCachedResources();
await refreshCachedOffers();
clearMessage();
return database;
}
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return createSqlDatabase(params);
case DefaultAccountExperienceType.MongoDB:
return createMongoDatabase(params);
case DefaultAccountExperienceType.Cassandra:
return createCassandraKeyspace(params);
case DefaultAccountExperienceType.Graph:
return createGremlineDatabase(params);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getMongoDBDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateMongoDBDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getGremlinDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateGremlinDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
const createBody: DatabaseRequest = { id: params.databaseId };
const databaseOptions: RequestOptions = {};
// TODO: replace when SDK support autopilot
if (params.databaseLevelThroughput) {
if (params.autoPilotMaxThroughput) {
createBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createBody.throughput = params.offerThroughput;
}
}
const response: DatabaseResponse = await client().databases.create(createBody, databaseOptions);
return response.resource;
}
function constructRpOptions(params: DataModels.CreateDatabaseParams): CreateUpdateOptions {
if (!params.databaseLevelThroughput) {
return {};
}
if (params.autoPilotMaxThroughput) {
return {
autoscaleSettings: {
maxThroughput: params.autoPilotMaxThroughput
}
};
}
return {
throughput: params.offerThroughput
};
}

View File

@@ -0,0 +1,46 @@
jest.mock("../../Utils/arm/request");
jest.mock("../MessageHandler");
jest.mock("../CosmosClient");
import { deleteCollection } from "./deleteCollection";
import { armRequest } from "../../Utils/arm/request";
import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteCollection", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await deleteCollection("database", "collection");
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
container: () => {
return {
delete: (): unknown => undefined
};
}
};
}
});
await deleteCollection("database", "collection");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,57 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteCollectionWithARM(databaseId, collectionId);
} else {
await client()
.database(databaseId)
.container(collectionId)
.delete();
}
} catch (error) {
logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteCollection", error.code);
sendNotificationForError(error);
throw error;
}
logConsoleInfo(`Successfully deleted container ${collectionId}`);
clearMessage();
await refreshCachedResources();
}
function deleteCollectionWithARM(databaseId: string, collectionId: string): Promise<void> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return deleteSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.MongoDB:
return deleteMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.Cassandra:
return deleteCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.Graph:
return deleteGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
case DefaultAccountExperienceType.Table:
return deleteTable(subscriptionId, resourceGroup, accountName, collectionId);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}

View File

@@ -0,0 +1,42 @@
jest.mock("../../Utils/arm/request");
jest.mock("../MessageHandler");
jest.mock("../CosmosClient");
import { deleteDatabase } from "./deleteDatabase";
import { armRequest } from "../../Utils/arm/request";
import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteDatabase", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await deleteDatabase("database");
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
delete: (): unknown => undefined
};
}
});
await deleteDatabase("database");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,58 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function deleteDatabase(databaseId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
try {
if (
window.authType === AuthType.AAD &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
!userContext.useSDKOperations
) {
await deleteDatabaseWithARM(databaseId);
} else {
await client()
.database(databaseId)
.delete();
}
} catch (error) {
logConsoleError(`Error while deleting database ${databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteDatabase", error.code);
sendNotificationForError(error);
throw error;
}
logConsoleInfo(`Successfully deleted database ${databaseId}`);
clearMessage();
await refreshCachedResources();
}
function deleteDatabaseWithARM(databaseId: string): Promise<void> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return deleteSqlDatabase(subscriptionId, resourceGroup, accountName, databaseId);
case DefaultAccountExperienceType.MongoDB:
return deleteMongoDBDatabase(subscriptionId, resourceGroup, accountName, databaseId);
case DefaultAccountExperienceType.Cassandra:
return deleteCassandraKeyspace(subscriptionId, resourceGroup, accountName, databaseId);
case DefaultAccountExperienceType.Graph:
return deleteGremlinDatabase(subscriptionId, resourceGroup, accountName, databaseId);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}

View File

@@ -0,0 +1,35 @@
jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient";
import { readCollection } from "./readCollection";
import { updateUserContext } from "../../UserContext";
describe("readCollection", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
});
it("should call SDK if logged in with resource token", async () => {
window.authType = AuthType.ResourceToken;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
container: () => {
return {
read: (): unknown => ({})
};
}
};
}
});
await readCollection("database", "collection");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,24 @@
import * as DataModels from "../../Contracts/DataModels";
import { client } from "../CosmosClient";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
let collection: DataModels.Collection;
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.read();
collection = response.resource as DataModels.Collection;
} catch (error) {
logConsoleError(`Error while querying container ${collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollection", error.code);
sendNotificationForError(error);
throw error;
}
clearMessage();
return collection;
}

View File

@@ -0,0 +1,45 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
import { readCollections } from "./readCollections";
import { updateUserContext } from "../../UserContext";
describe("readCollections", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await readCollections("database");
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
database: () => {
return {
containers: {
readAll: () => {
return {
fetchAll: (): unknown => []
};
}
}
};
}
});
await readCollections("database");
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,71 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
let collections: DataModels.Collection[];
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
collections = await readCollectionsWithARM(databaseId);
} else {
const sdkResponse = await client()
.database(databaseId)
.containers.readAll()
.fetchAll();
collections = sdkResponse.resources as DataModels.Collection[];
}
} catch (error) {
logConsoleError(`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollections", error.code);
sendNotificationForError(error);
throw error;
}
clearMessage();
return collections;
}
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await listSqlContainers(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await listMongoDBCollections(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await listCassandraTables(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await listGremlinGraphs(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Table:
rpResponse = await listTables(subscriptionId, resourceGroup, accountName);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.value?.map(collection => collection.properties?.resource as DataModels.Collection);
}

View File

@@ -0,0 +1,41 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
import { readDatabases } from "./readDatabases";
import { updateUserContext } from "../../UserContext";
describe("readDatabases", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await readDatabases();
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
databases: {
readAll: () => {
return {
fetchAll: (): unknown => []
};
}
}
});
await readDatabases();
expect(client).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,67 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient";
import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`);
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
) {
databases = await readDatabasesWithARM();
} else {
const sdkResponse = await client()
.databases.readAll()
.fetchAll();
databases = sdkResponse.resources as DataModels.Database[];
}
} catch (error) {
logConsoleError(`Error while querying databases:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadDatabases", error.code);
sendNotificationForError(error);
throw error;
}
clearMessage();
return databases;
}
async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.value?.map(database => database.properties?.resource as DataModels.Database);
}

View File

@@ -0,0 +1,20 @@
import * as Constants from "../Constants";
import { sendMessage } from "../MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
interface CosmosError {
code: number;
message?: string;
}
export function sendNotificationForError(error: CosmosError): void {
if (error && error.code === Constants.HttpStatusCodes.Forbidden) {
if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
sendMessage({
type: MessageTypes.ForbiddenError,
reason: error && error.message ? error.message : error
});
}
}

View File

@@ -0,0 +1,225 @@
import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import {
ExtendedResourceProperties,
SqlContainerCreateUpdateParameters,
SqlContainerResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import {
createUpdateCassandraTable,
getCassandraTable
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import {
createUpdateGremlinGraph,
getGremlinGraph
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function updateCollection(
databaseId: string,
collectionId: string,
newCollection: Collection,
options: RequestOptions = {}
): Promise<Collection> {
let collection: Collection;
const clearMessage = logConsoleProgress(`Updating container ${collectionId}`);
try {
if (
window.authType === AuthType.AAD &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else {
const sdkResponse = await client()
.database(databaseId)
.container(collectionId)
.replace(newCollection as ContainerDefinition, options);
collection = sdkResponse.resource as Collection;
}
} catch (error) {
logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
throw error;
}
logConsoleInfo(`Successfully updated container ${collectionId}`);
clearMessage();
await refreshCachedResources();
return collection;
}
async function updateCollectionWithARM(
databaseId: string,
collectionId: string,
newCollection: Collection
): Promise<Collection> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return updateSqlContainer(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
case DefaultAccountExperienceType.Cassandra:
return updateCassandraTable(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Graph:
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Table:
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}
async function updateSqlContainer(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateSqlContainer(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
}
async function updateMongoDBCollection(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateMongoDBCollection(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(
`MongoDB collection to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
);
}
async function updateCassandraTable(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateCassandraTable(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(
`Cassandra table to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
);
}
async function updateGremlinGraph(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateGremlinGraph(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(`Gremlin graph to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
}
async function updateTable(
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateTable(
subscriptionId,
resourceGroup,
accountName,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(`Table to update does not exist. Table id: ${collectionId}`);
}

View File

@@ -0,0 +1,26 @@
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
describe("updateOfferThroughputBeyondLimit", () => {
it("should call fetch", async () => {
window.fetch = jest.fn(() => {
return {
ok: true
};
});
window.dataExplorer = {
logConsoleData: jest.fn(),
deleteInProgressConsoleDataWithId: jest.fn(),
extensionEndpoint: jest.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
await updateOfferThroughputBeyondLimit({
subscriptionId: "foo",
resourceGroup: "foo",
databaseAccountName: "foo",
databaseName: "foo",
throughput: 1000000000,
offerIsRUPerMinuteThroughputEnabled: false
});
expect(window.fetch).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,52 @@
import { Platform, configContext } from "../../ConfigContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { HttpHeaders } from "../Constants";
interface UpdateOfferThroughputRequest {
subscriptionId: string;
resourceGroup: string;
databaseAccountName: string;
databaseName: string;
collectionName?: string;
throughput: number;
offerIsRUPerMinuteThroughputEnabled: boolean;
offerAutopilotSettings?: AutoPilotOfferSettings;
}
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
if (configContext.platform !== Platform.Portal) {
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
}
const resourceDescriptionInfo = request.collectionName
? `database ${request.databaseName} and container ${request.collectionName}`
: `database ${request.databaseName}`;
const clearMessage = logConsoleProgress(
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
);
const explorer = window.dataExplorer;
const url = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
const authorizationHeader = getAuthorizationHeader();
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(request),
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
});
if (response.ok) {
logConsoleInfo(
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
);
clearMessage();
return undefined;
}
const error = await response.json();
logConsoleError(`Failed to request an increase in throughput for ${request.throughput}: ${error.message}`);
clearMessage();
throw new Error(error.message);
}

View File

@@ -4,7 +4,7 @@ export enum Platform {
Emulator = "Emulator"
}
interface Config {
interface ConfigContext {
platform: Platform;
allowedParentFrameOrigins: RegExp;
gitSha?: string;
@@ -28,7 +28,7 @@ interface Config {
}
// Default configuration
let config: Config = {
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedParentFrameOrigins: /^https:\/\/portal\.azure\.com$|^https:\/\/portal\.azure\.us$|^https:\/\/portal\.azure\.cn$|^https:\/\/portal\.microsoftazure\.de$|^https:\/\/.+\.portal\.azure\.com$|^https:\/\/.+\.portal\.azure\.us$|^https:\/\/.+\.portal\.azure\.cn$|^https:\/\/.+\.portal\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.com$|^https:\/\/main\.documentdb\.ext\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.cn$|^https:\/\/main\.documentdb\.ext\.azure\.us$/,
// Webpack injects this at build time
@@ -46,36 +46,58 @@ let config: Config = {
JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
};
export function resetConfigContext(): void {
if (process.env.NODE_ENV !== "test") {
throw new Error("resetConfigContext can only becalled in a test environment");
}
configContext = {} as ConfigContext;
}
export function updateConfigContext(newContext: Partial<ConfigContext>): void {
Object.assign(configContext, newContext);
}
// Injected for local develpment. These will be removed in the production bundle by webpack
if (process.env.NODE_ENV === "development") {
const port: string = process.env.PORT || "1234";
config.BACKEND_ENDPOINT = "https://localhost:" + port;
config.MONGO_BACKEND_ENDPOINT = "https://localhost:" + port;
config.PROXY_PATH = "/proxy";
config.EMULATOR_ENDPOINT = "https://localhost:8081";
updateConfigContext({
BACKEND_ENDPOINT: "https://localhost:" + port,
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: "https://localhost:8081"
});
}
export async function initializeConfiguration(): Promise<Config> {
export async function initializeConfiguration(): Promise<ConfigContext> {
try {
const response = await fetch("./config.json");
if (response.status === 200) {
try {
const externalConfig = await response.json();
config = Object.assign({}, config, externalConfig);
Object.assign(configContext, externalConfig);
} catch (error) {
console.error("Unable to parse json in config file");
console.error(error);
}
}
// Allow override of any config value with URL query parameters
// Allow override of platform value with URL query parameter
const params = new URLSearchParams(window.location.search);
params.forEach((value, key) => {
(config as any)[key] = value;
});
if (params.has("platform")) {
const platform = params.get("platform");
switch (platform) {
default:
console.log("Invalid platform query parameter given, ignoring");
break;
case Platform.Portal:
case Platform.Hosted:
case Platform.Emulator:
updateConfigContext({ platform });
}
}
} catch (error) {
console.log("No configuration file found using defaults");
}
return config;
return configContext;
}
export { config };
export { configContext };

View File

@@ -153,7 +153,14 @@ export interface KeyResource {
Token: string;
}
export interface IndexingPolicy {}
export interface IndexingPolicy {
automatic: boolean;
indexingMode: string;
includedPaths: any;
excludedPaths: any;
compositeIndexes?: any;
spatialIndexes?: any;
}
export interface PartitionKey {
paths: string[];
@@ -312,17 +319,6 @@ export interface Query {
query: string;
}
export interface UpdateOfferThroughputRequest {
subscriptionId: string;
resourceGroup: string;
databaseAccountName: string;
databaseName: string;
collectionName: string;
throughput: number;
offerIsRUPerMinuteThroughputEnabled: boolean;
offerAutopilotSettings?: AutoPilotOfferSettings;
}
export interface AutoPilotOfferSettings {
tier?: AutopilotTier;
maximumTierThroughput?: number;
@@ -331,12 +327,11 @@ export interface AutoPilotOfferSettings {
targetMaxThroughput?: number;
}
export interface CreateDatabaseRequest {
export interface CreateDatabaseParams {
autoPilotMaxThroughput?: number;
databaseId: string;
databaseLevelThroughput?: boolean;
offerThroughput?: number;
autoPilot?: AutoPilotCreationSettings;
hasAutoPilotV2FeatureFlag?: boolean;
}
export interface SharedThroughputRange {
@@ -437,10 +432,8 @@ export interface Tenant {
export interface AccountKeys {
primaryMasterKey: string;
secondaryMasterKey: string;
properties: {
primaryReadonlyMasterKey: string;
secondaryReadonlyMasterKey: string;
};
primaryReadonlyMasterKey: string;
secondaryReadonlyMasterKey: string;
}
export interface AfecFeature {

View File

@@ -1,306 +1,16 @@
import * as DataModels from "./DataModels";
import * as Entities from "../Explorer/Tables/Entities";
import * as monaco from "monaco-editor";
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase";
import Q from "q";
import QueryViewModel from "../Explorer/Tables/QueryBuilder/QueryViewModel";
import TableEntityListViewModel from "../Explorer/Tables/DataTable/TableEntityListViewModel";
import { AccessibleVerticalList } from "../Explorer/Tree/AccessibleVerticalList";
import { ArcadiaWorkspaceItem } from "../Explorer/Controls/Arcadia/ArcadiaMenuPicker";
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../Explorer/Tables/TableDataClient";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { ExecuteSprocParam } from "../Explorer/Panes/ExecuteSprocParamsPane";
import { GitHubClient } from "../GitHub/GitHubClient";
import { IColumnSetting } from "../Explorer/Panes/Tables/TableColumnOptionsPane";
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
import { Library } from "./DataModels";
import { MostRecentActivity } from "../Explorer/MostRecentActivity/MostRecentActivity";
import { NotebookContentItem } from "../Explorer/Notebook/NotebookContentItem";
import { PlatformType } from "../PlatformType";
import { QueryMetrics } from "@azure/cosmos";
import { SetupNotebooksPane } from "../Explorer/Panes/SetupNotebooksPane";
import { Splitter } from "../Common/Splitter";
import { StringInputPane } from "../Explorer/Panes/StringInputPane";
import { TabsManager } from "../Explorer/Tabs/TabsManager";
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
import { UploadDetails } from "../workers/upload/definitions";
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
export interface ExplorerOptions {
documentClientUtility: DocumentClientUtilityBase;
notificationsClient: NotificationsClient;
isEmulator: boolean;
}
export interface Capability extends DataModels.Capability {}
export interface ConfigurationOverrides extends DataModels.ConfigurationOverrides {}
export interface NavbarButtonConfig extends CommandButtonComponentProps {}
export interface DatabaseAccount extends DataModels.DatabaseAccount {}
export interface Explorer {
flight: ko.Observable<string>;
handleMessage(event: MessageEvent): void;
isRefreshingExplorer: ko.Observable<boolean>;
databaseAccount: ko.Observable<DatabaseAccount>;
subscriptionType: ko.Observable<SubscriptionType>;
quotaId: ko.Observable<string>;
hasWriteAccess: ko.Observable<boolean>;
defaultExperience: ko.Observable<string>;
isPreferredApiDocumentDB: ko.Computed<boolean>;
isPreferredApiCassandra: ko.Computed<boolean>;
isPreferredApiTable: ko.Computed<boolean>;
isPreferredApiGraph: ko.Computed<boolean>;
isPreferredApiMongoDB: ko.Computed<boolean>;
isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
isDatabaseNodeOrNoneSelected(): boolean;
isDatabaseNodeSelected(): boolean;
isNodeKindSelected(nodeKind: string): boolean;
isNoneSelected(): boolean;
isSelectedDatabaseShared(): boolean;
deleteDatabaseText: ko.Observable<string>;
deleteCollectionText: ko.Subscribable<string>; // Our code assigns to a ko.Observable, but unit test assigns to ko.Computed
addCollectionText: ko.Observable<string>;
addDatabaseText: ko.Observable<string>;
collectionTitle: ko.Observable<string>;
collectionTreeNodeAltText: ko.Observable<string>;
refreshTreeTitle: ko.Observable<string>;
isAccountReady: ko.Observable<boolean>;
collectionCreationDefaults: CollectionCreationDefaults;
isEmulator: boolean;
features: ko.Observable<any>;
serverId: ko.Observable<string>;
extensionEndpoint: ko.Observable<string>;
armEndpoint: ko.Observable<string>;
isFeatureEnabled: (feature: string) => boolean;
isGalleryPublishEnabled: ko.Computed<boolean>;
isGitHubPaneEnabled: ko.Observable<boolean>;
isPublishNotebookPaneEnabled: ko.Observable<boolean>;
isRightPanelV2Enabled: ko.Computed<boolean>;
canExceedMaximumValue: ko.Computed<boolean>;
hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
isHostedDataExplorerEnabled: ko.Computed<boolean>;
isNotificationConsoleExpanded: ko.Observable<boolean>;
isTryCosmosDBSubscription: ko.Observable<boolean>;
canSaveQueries: ko.Computed<boolean>;
parentFrameDataExplorerVersion: ko.Observable<string>;
documentClientUtility: DocumentClientUtilityBase;
notificationsClient: NotificationsClient;
queriesClient: QueriesClient;
tableDataClient: TableDataClient;
splitter: Splitter;
notificationConsoleData: ko.ObservableArray<ConsoleData>;
// Selection
selectedNode: ko.Observable<TreeNode>;
// Tree
databases: ko.ObservableArray<Database>;
nonSystemDatabases: ko.Computed<Database[]>;
selectedDatabaseId: ko.Computed<string>;
selectedCollectionId: ko.Computed<string>;
isLeftPaneExpanded: ko.Observable<boolean>;
// Resource Token
resourceTokenDatabaseId: ko.Observable<string>;
resourceTokenCollectionId: ko.Observable<string>;
resourceTokenCollection: ko.Observable<CollectionBase>;
resourceTokenPartitionKey: ko.Observable<string>;
isAuthWithResourceToken: ko.Observable<boolean>;
isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
// Tabs
isTabsContentExpanded: ko.Observable<boolean>;
tabsManager: TabsManager;
// Contextual Panes
addDatabasePane: AddDatabasePane;
addCollectionPane: AddCollectionPane;
deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane;
graphStylingPane: GraphStylingPane;
addTableEntityPane: AddTableEntityPane;
editTableEntityPane: EditTableEntityPane;
tableColumnOptionsPane: TableColumnOptionsPane;
querySelectPane: QuerySelectPane;
newVertexPane: NewVertexPane;
cassandraAddCollectionPane: CassandraAddCollectionPane;
settingsPane: SettingsPane;
executeSprocParamsPane: ExecuteSprocParamsPane;
renewAdHocAccessPane: RenewAdHocAccessPane;
uploadItemsPane: UploadItemsPane;
uploadItemsPaneAdapter: UploadItemsPaneAdapter;
loadQueryPane: LoadQueryPane;
saveQueryPane: ContextualPane;
browseQueriesPane: BrowseQueriesPane;
uploadFilePane: UploadFilePane;
stringInputPane: StringInputPane;
setupNotebooksPane: SetupNotebooksPane;
libraryManagePane: ContextualPane;
clusterLibraryPane: ContextualPane;
gitHubReposPane: ContextualPane;
publishNotebookPaneAdapter: ReactAdapter;
// Facade
logConsoleData(data: ConsoleData): void;
isNodeKindSelected(nodeKind: string): boolean;
initDataExplorerWithFrameInputs(inputs: DataExplorerInputsFrame): Q.Promise<void>;
toggleLeftPaneExpanded(): void;
refreshDatabaseForResourceToken(): Q.Promise<void>;
refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any>;
closeAllPanes(): void;
findSelectedDatabase(): Database;
findDatabaseWithId(databaseRid: string): Database;
isLastDatabase(): boolean;
isLastNonEmptyDatabase(): boolean;
findSelectedCollection(): Collection;
isLastCollection(): boolean;
findSelectedStoredProcedure(): StoredProcedure;
findSelectedUDF(): UserDefinedFunction;
findSelectedTrigger(): Trigger;
findCollection(rid: string): Collection;
provideFeedbackEmail(): void;
expandConsole: () => void;
collapseConsole: () => void;
generateSharedAccessData(): void;
getPlatformType(): PlatformType;
isConnectExplorerVisible(): boolean;
isRunningOnNationalCloud(): boolean;
displayConnectExplorerForm(): void;
hideConnectExplorerForm(): void;
displayContextSwitchPromptForConnectionString(connectionString: string): void;
displayGuestAccessTokenRenewalPrompt(): void;
rebindDocumentClientUtility(documentClientUtility: DocumentClientUtilityBase): void;
renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise<void>;
renewShareAccess(accessInput: string): Q.Promise<void>;
onUpdateTabsButtons: (buttons: NavbarButtonConfig[]) => void;
onNewCollectionClicked: () => void;
showOkModalDialog: (title: string, msg: string) => void;
showOkCancelModalDialog: (
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void
) => void;
showOkCancelTextFieldModalDialog: (
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
textFiledProps: TextFieldProps,
isPrimaryButtonDisabled?: boolean
) => void;
// Analytics
isNotebookEnabled: ko.Observable<boolean>;
isSparkEnabled: ko.Observable<boolean>;
isNotebooksEnabledForAccount: ko.Observable<boolean>;
isSparkEnabledForAccount: ko.Observable<boolean>;
hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
openEnableSynapseLinkDialog(): void;
isSynapseLinkUpdating: ko.Observable<boolean>;
notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
arcadiaToken: ko.Observable<string>;
arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
notebookManager?: any; // This is dynamically loaded
openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean>; // True if it was opened, false otherwise
resetNotebookWorkspace(): void;
importAndOpen: (path: string) => Promise<boolean>;
importAndOpenContent: (name: string, content: string) => Promise<boolean>;
publishNotebook: (name: string, content: string) => void;
openNotebookTerminal: (kind: TerminalKind) => void;
openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
openNotebookViewer: (notebookUrl: string) => void;
notebookWorkspaceManager: NotebookWorkspaceManager;
sparkClusterManager: SparkClusterManager;
mostRecentActivity: MostRecentActivity;
initNotebooks: (databaseAccount: DataModels.DatabaseAccount) => Promise<void>;
deleteCluster(): void;
openSparkMasterTab(): Promise<void>;
handleOpenFileAction(path: string): Promise<void>;
// Notebook operations
openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean>; // True if it was opened, false otherwise
deleteNotebookFile: (item: NotebookContentItem) => Promise<void>;
onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem>;
onNewNotebookClicked: (parent?: NotebookContentItem) => void;
onUploadToNotebookServerClicked: (parent?: NotebookContentItem) => void;
renameNotebook: (notebookFile: NotebookContentItem) => Q.Promise<NotebookContentItem>;
readFile: (notebookFile: NotebookContentItem) => Promise<string>;
downloadFile: (notebookFile: NotebookContentItem) => Promise<void>;
createNotebookContentItemFile: (name: string, filepath: string) => NotebookContentItem;
refreshContentItem(item: NotebookContentItem): Promise<void>;
getNotebookBasePath(): string;
createWorkspace(): Promise<string>;
createSparkPool(workspaceId: string): Promise<string>;
}
export interface NotebookWorkspaceManager {
getNotebookWorkspacesAsync(cosmosAccountResourceId: string): Promise<DataModels.NotebookWorkspace[]>;
getNotebookWorkspaceAsync(
cosmosAccountResourceId: string,
notebookWorkspaceId: string
): Promise<DataModels.NotebookWorkspace>;
createNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise<void>;
deleteNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise<void>;
getNotebookConnectionInfoAsync(
cosmosAccountResourceId: string,
notebookWorkspaceId: string
): Promise<DataModels.NotebookWorkspaceConnectionInfo>;
startNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise<void>;
}
export interface KernelConnectionMetadata {
name: string;
configurationEndpoints: DataModels.NotebookConfigurationEndpoints;
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
}
export interface SparkClusterManager {
getClustersAsync(cosmosAccountResourceId: string): Promise<DataModels.SparkCluster[]>;
getClusterAsync(cosmosAccountResourceId: string, clusterId: string): Promise<DataModels.SparkCluster>;
createClusterAsync(cosmosAccountResourceId: string, cluster: Partial<DataModels.SparkCluster>): Promise<void>;
updateClusterAsync(
cosmosAccountResourceId: string,
clusterId: string,
sparkCluster: DataModels.SparkCluster
): Promise<DataModels.SparkCluster>;
deleteClusterAsync(cosmosAccountResourceId: string, clusterId: string): Promise<void>;
getClusterConnectionInfoAsync(
cosmosAccountResourceId: string,
clusterId: string
): Promise<DataModels.SparkClusterConnectionInfo>;
getLibrariesAsync(cosmosdbResourceId: string): Promise<Library[]>;
getLibraryAsync(cosmosdbResourceId: string, libraryName: string): Promise<Library>;
addLibraryAsync(cosmosdbResourceId: string, libraryName: string, library: Library): Promise<void>;
deleteLibraryAsync(cosmosdbResourceId: string, libraryName: string): Promise<void>;
}
export interface ArcadiaResourceManager {
getWorkspacesAsync(arcadiaResourceId: string): Promise<DataModels.ArcadiaWorkspace[]>;
getWorkspaceAsync(arcadiaResourceId: string, workspaceId: string): Promise<DataModels.ArcadiaWorkspace>;
listWorkspacesAsync(subscriptionIds: string[]): Promise<DataModels.ArcadiaWorkspace[]>;
listSparkPoolsAsync(resourceId: string): Promise<DataModels.SparkPool[]>;
}
import Explorer from "../Explorer/Explorer";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger";
import DocumentId from "../Explorer/Tree/DocumentId";
import ConflictId from "../Explorer/Tree/ConflictId";
export interface TokenProvider {
getAuthHeader(): Promise<Headers>;
@@ -340,11 +50,6 @@ export interface WaitsForTemplate {
isTemplateReady: ko.Observable<boolean>;
}
export interface AdHocAccessData {
readWriteUrl: string;
readUrl: string;
}
export interface TreeNode {
nodeKind: string;
rid: string;
@@ -466,236 +171,15 @@ export interface Collection extends CollectionBase {
getLabel(): string;
}
export interface DocumentId {
container: DocumentsTab;
rid: string;
self: string;
ts: string;
partitionKeyValue: any;
partitionKeyProperty: string;
partitionKey: DataModels.PartitionKey;
stringPartitionKeyValue: string;
id: ko.Observable<string>;
isDirty: ko.Observable<boolean>;
click(): void;
getPartitionKeyValueAsString(): string;
loadDocument(): Q.Promise<any>;
partitionKeyHeader(): Object;
}
export interface ConflictId {
container: ConflictsTab;
rid: string;
self: string;
ts: string;
partitionKeyValue: any;
partitionKeyProperty: string;
partitionKey: DataModels.PartitionKey;
stringPartitionKeyValue: string;
id: ko.Observable<string>;
operationType: string;
resourceId: string;
resourceType: string;
isDirty: ko.Observable<boolean>;
click(): void;
buildDocumentIdFromConflict(partitionKeyValue: any): DocumentId;
getPartitionKeyValueAsString(): string;
loadConflict(): Q.Promise<any>;
}
export interface StoredProcedure extends TreeNode {
container: Explorer;
collection: Collection;
rid: string;
self: string;
id: ko.Observable<string>;
body: ko.Observable<string>;
delete(): void;
open: () => void;
select(): void;
execute(params: string[], partitionKeyValue?: string): void;
}
export interface UserDefinedFunction extends TreeNode {
container: Explorer;
collection: Collection;
rid: string;
self: string;
id: ko.Observable<string>;
body: ko.Observable<string>;
delete(): void;
open: () => void;
select(): void;
}
export interface Trigger extends TreeNode {
container: Explorer;
collection: Collection;
rid: string;
self: string;
id: ko.Observable<string>;
body: ko.Observable<string>;
triggerType: ko.Observable<string>;
triggerOperation: ko.Observable<string>;
delete(): void;
open: () => void;
select(): void;
}
/**
* Options used to initialize pane
*/
export interface PaneOptions {
id: string;
documentClientUtility: DocumentClientUtilityBase;
visible: ko.Observable<boolean>;
container?: Explorer;
}
export interface ContextualPane {
documentClientUtility: DocumentClientUtilityBase;
formErrors: ko.Observable<string>;
formErrorsDetails: ko.Observable<string>;
id: string;
title: ko.Observable<string>;
visible: ko.Observable<boolean>;
firstFieldHasFocus: ko.Observable<boolean>;
isExecuting: ko.Observable<boolean>;
submit: () => void;
cancel: () => void;
open: () => void;
close: () => void;
resetData: () => void;
showErrorDetails: () => void;
onCloseKeyPress(source: any, event: KeyboardEvent): void;
onPaneKeyDown(source: any, event: KeyboardEvent): boolean;
}
export interface GitHubReposPaneOptions extends PaneOptions {
gitHubClient: GitHubClient;
junoClient: JunoClient;
}
export interface PublishNotebookPaneOptions extends PaneOptions {
junoClient: JunoClient;
}
export interface PublishNotebookPaneOpenOptions {
name: string;
author: string;
content: string;
}
export interface AddCollectionPaneOptions extends PaneOptions {
isPreferredApiTable: ko.Computed<boolean>;
databaseId?: string;
databaseSelfLink?: string;
}
export interface AddCollectionPane extends ContextualPane {
collectionIdTitle: ko.Observable<string>;
collectionWithThroughputInSharedTitle: ko.Observable<string>;
databaseId: ko.Observable<string>;
partitionKey: ko.Observable<string>;
storage: ko.Observable<string>;
throughputSinglePartition: ko.Observable<number>;
throughputMultiPartition: ko.Observable<number>;
open: (databaseId?: string) => void;
onStorageOptionsKeyDown(source: any, event: KeyboardEvent): boolean;
onRupmOptionsKeyDown(source: any, event: KeyboardEvent): void;
onEnableSynapseLinkButtonClicked: () => void;
}
export interface AddDatabasePane extends ContextualPane {}
export interface DeleteDatabaseConfirmationPane extends ContextualPane {
databaseIdConfirmation: ko.Observable<string>;
databaseIdConfirmationText: ko.Observable<string>;
databaseDeleteFeedback: ko.Observable<string>;
}
export interface DeleteCollectionConfirmationPane extends ContextualPane {
collectionIdConfirmation: ko.Observable<string>;
collectionIdConfirmationText: ko.Observable<string>;
containerDeleteFeedback: ko.Observable<string>;
recordDeleteFeedback: ko.Observable<boolean>;
}
export interface SettingsPane extends ContextualPane {
pageOption: ko.Observable<string>;
customItemPerPage: ko.Observable<number>;
crossPartitionQueryEnabled: ko.Observable<boolean>;
maxDegreeOfParallelism: ko.Observable<number>;
shouldShowQueryPageOptions: ko.Computed<boolean>;
onCustomPageOptionsKeyDown(source: any, event: KeyboardEvent): boolean;
onUnlimitedPageOptionKeyDown(source: any, event: KeyboardEvent): boolean;
onJsonDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean;
onGraphDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean;
}
export interface ExecuteSprocParamsPane extends ContextualPane {
params: ko.ObservableArray<ExecuteSprocParam>;
partitionKeyValue: ko.Observable<string>;
addNewParam(): void;
}
export interface RenewAdHocAccessPane extends ContextualPane {
accessKey: ko.Observable<string>;
}
export interface UploadItemsPane extends ContextualPane {
selectedFilesTitle: ko.Observable<string>;
files: ko.Observable<FileList>;
updateSelectedFiles(element: any, event: any): void;
}
export interface LoadQueryPane extends ContextualPane {
selectedFilesTitle: ko.Observable<string>;
files: ko.Observable<FileList>;
loadQueryFromFile(file: File): Q.Promise<void>;
}
export interface BrowseQueriesPane extends ContextualPane {
loadSavedQuery: (savedQuery: DataModels.Query) => void;
}
export interface UploadFilePaneOpenOptions {
paneTitle: string;
selectFileInputLabel: string;
errorMessage: string; // Could not upload notebook
inProgressMessage: string; // Uploading notebook
successMessage: string; // Successfully uploaded notebook
onSubmit: (file: File) => Promise<any>;
extensions?: string; // input accept field. E.g: .ipynb
submitButtonLabel?: string;
}
export interface StringInputPaneOpenOptions {
paneTitle: string;
inputLabel: string;
errorMessage: string;
inProgressMessage: string;
successMessage: string;
onSubmit: (input: string) => Promise<any>;
submitButtonLabel: string;
defaultInput?: string;
}
export interface UploadFilePane extends ContextualPane {
openWithOptions: (options: UploadFilePaneOpenOptions) => void;
}
/**
* Graph configuration
*/
@@ -740,41 +224,6 @@ export interface InputProperty {
values: InputPropertyValue[];
}
export interface GraphStylingPane extends ContextualPane {
setData(graphConfigUIData: GraphConfigUiData): void;
}
export interface NewVertexPane extends ContextualPane {
setPartitionKeyProperty: (pKeyProp: string) => void;
subscribeOnSubmitCreate: (callback: (newVertexData: NewVertexData) => void) => void;
}
export interface AddTableEntityPane extends ContextualPane {
tableViewModel: TableEntityListViewModel;
}
export interface EditTableEntityPane extends ContextualPane {
originEntity: Entities.ITableEntity;
tableViewModel: TableEntityListViewModel;
originalNumberOfProperties: number;
}
export interface TableColumnOptionsPane extends ContextualPane {
tableViewModel: TableEntityListViewModel;
parameters: IColumnSetting;
setDisplayedColumns(columnNames: string[], order: number[], visible: boolean[]): void;
}
export interface QuerySelectPane extends ContextualPane {
queryViewModel: QueryViewModel;
}
export interface CassandraAddCollectionPane extends ContextualPane {
createTableQuery: ko.Observable<string>;
keyspaceId: ko.Observable<string>;
userTableQuery: ko.Observable<string>;
}
export interface Editable<T> extends ko.Observable<T> {
setBaseline(baseline: T): void;
@@ -800,19 +249,6 @@ export interface DocumentRequestContainer {
resourceName?: string;
}
export interface NotificationsClient {
fetchNotifications(): Q.Promise<DataModels.Notification[]>;
setExtensionEndpoint(extensionEndpoint: string): void;
}
export interface QueriesClient {
setupQueriesCollection(): Promise<DataModels.Collection>;
saveQuery(query: DataModels.Query): Promise<void>;
getQueries(): Promise<DataModels.Query[]>;
deleteQuery(query: DataModels.Query): Promise<void>;
getResourceId(): string;
}
export interface DocumentClientOption {
endpoint?: string;
masterKey?: string;
@@ -824,11 +260,10 @@ export interface TabOptions {
tabKind: CollectionTabKind;
title: string;
tabPath: string;
documentClientUtility: DocumentClientUtilityBase;
selfLink: string;
isActive: ko.Observable<boolean>;
hashLocation: string;
onUpdateTabsButtons: (buttons: NavbarButtonConfig[]) => void;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>;
onLoadStartKey?: number;
@@ -841,47 +276,6 @@ export interface TabOptions {
theme?: string;
}
export interface SparkMasterTabOptions extends TabOptions {
clusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
container: Explorer;
}
export interface GraphTabOptions extends TabOptions {
account: DatabaseAccount;
masterKey: string;
collectionId: string;
databaseId: string;
collectionPartitionKeyProperty: string;
}
export interface NotebookTabOptions extends TabOptions {
account: DatabaseAccount;
masterKey: string;
container: Explorer;
notebookContentItem: NotebookContentItem;
}
export interface TerminalTabOptions extends TabOptions {
account: DatabaseAccount;
container: Explorer;
kind: TerminalKind;
}
export interface GalleryTabOptions extends TabOptions {
account: DatabaseAccount;
container: Explorer;
junoClient: JunoClient;
notebookUrl?: string;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
}
export interface NotebookViewerTabOptions extends TabOptions {
account: DatabaseAccount;
container: Explorer;
notebookUrl: string;
}
export interface DocumentsTabOptions extends TabOptions {
partitionKey: DataModels.PartitionKey;
documentIds: ko.ObservableArray<DocumentId>;
@@ -909,262 +303,15 @@ export interface ScriptTabOption extends TabOptions {
partitionKey?: DataModels.PartitionKey;
}
// Tabs
export interface Tab {
documentClientUtility: DocumentClientUtilityBase;
node: TreeNode; // Can be null
collection: CollectionBase;
rid: string;
tabKind: CollectionTabKind;
tabId: string;
isActive: ko.Observable<boolean>;
isMouseOver: ko.Observable<boolean>;
tabPath: ko.Observable<string>;
tabTitle: ko.Observable<string>;
hashLocation: ko.Observable<string>;
closeTabButton: Button;
onCloseTabButtonClick(): void;
onTabClick(): Q.Promise<any>;
onKeyPressActivate(source: any, event: KeyboardEvent): void;
onKeyPressClose(source: any, event: KeyboardEvent): void;
onActivate(): Q.Promise<any>;
refresh(): void;
closeButtonTabIndex: ko.Computed<number>;
isExecutionError: ko.Observable<boolean>;
isExecuting: ko.Observable<boolean>;
}
export interface DocumentsTab extends Tab {
/* Documents Grid */
selectDocument(documentId: DocumentId): Q.Promise<any>;
selectedDocumentId: ko.Observable<DocumentId>;
selectedDocumentContent: Editable<any>;
onDocumentIdClick(documentId: DocumentId): Q.Promise<any>;
dataContentsGridScrollHeight: ko.Observable<string>;
accessibleDocumentList: AccessibleVerticalList;
documentContentsGridId: string;
partitionKey: DataModels.PartitionKey;
idHeader: string;
partitionKeyPropertyHeader: string;
partitionKeyProperty: string;
documentIds: ko.ObservableArray<DocumentId>;
/* Documents Filter */
filterContent: ko.Observable<string>;
appliedFilter: ko.Observable<string>;
lastFilterContents: ko.ObservableArray<string>;
isFilterExpanded: ko.Observable<boolean>;
applyFilterButton: Button;
onShowFilterClick(): Q.Promise<any>;
onHideFilterClick(): Q.Promise<any>;
onApplyFilterClick(): Q.Promise<any>;
/* Document Editor */
isEditorDirty: ko.Computed<boolean>;
editorState: ko.Observable<DocumentExplorerState>;
onValidDocumentEdit(content: any): Q.Promise<any>;
onInvalidDocumentEdit(content: any): Q.Promise<any>;
onNewDocumentClick(): Q.Promise<any>;
onSaveNewDocumentClick(): Q.Promise<any>;
onRevertNewDocumentClick(): Q.Promise<any>;
onSaveExisitingDocumentClick(): Q.Promise<any>;
onRevertExisitingDocumentClick(): Q.Promise<any>;
onDeleteExisitingDocumentClick(): Q.Promise<any>;
/* Errors */
displayedError: ko.Observable<string>;
initDocumentEditor(documentId: DocumentId, content: any): Q.Promise<any>;
loadNextPage(): Q.Promise<any>;
}
export interface ConflictsTab extends Tab {
/* Conflicts Grid */
selectedConflictId: ko.Observable<ConflictId>;
selectedConflictContent: Editable<any>;
selectedConflictCurrent: Editable<any>;
onConflictIdClick(conflictId: ConflictId): Q.Promise<any>;
dataContentsGridScrollHeight: ko.Observable<string>;
accessibleDocumentList: AccessibleVerticalList;
documentContentsGridId: string;
partitionKey: DataModels.PartitionKey;
partitionKeyPropertyHeader: string;
partitionKeyProperty: string;
conflictIds: ko.ObservableArray<ConflictId>;
/* Document Editor */
isEditorDirty: ko.Computed<boolean>;
editorState: ko.Observable<DocumentExplorerState>;
onValidDocumentEdit(content: any): Q.Promise<any>;
onInvalidDocumentEdit(content: any): Q.Promise<any>;
loadingConflictData: ko.Observable<boolean>;
onAcceptChangesClick(): Q.Promise<any>;
onDiscardClick(): Q.Promise<any>;
initDocumentEditorForCreate(documentId: ConflictId, documentToInsert: any): Q.Promise<any>;
initDocumentEditorForReplace(documentId: ConflictId, conflictContent: any, currentContent: any): Q.Promise<any>;
initDocumentEditorForDelete(documentId: ConflictId, documentToDelete: any): Q.Promise<any>;
initDocumentEditorForNoOp(conflictId: ConflictId): Q.Promise<any>;
loadNextPage(): Q.Promise<any>;
}
export interface SettingsTab extends Tab {
/*state*/
throughput: ko.Observable<number>;
timeToLive: ko.Observable<string>;
timeToLiveSeconds: ko.Observable<number>;
geospatialVisible: ko.Computed<boolean>;
geospatialConfigType: ko.Observable<string>;
indexingPolicyContent: ko.Observable<DataModels.IndexingPolicy>;
rupm: ko.Observable<string>;
requestUnitsUsageCost: ko.Computed<string>;
canThroughputExceedMaximumValue: ko.Computed<boolean>;
shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
warningMessage: ko.Computed<string>;
ttlOffFocused: ko.Observable<boolean>;
ttlOnDefaultFocused: ko.Observable<boolean>;
ttlOnFocused: ko.Observable<boolean>;
indexingPolicyElementFocused: ko.Observable<boolean>;
notificationStatusInfo: ko.Observable<string>;
shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
shouldShowStatusBar: ko.Computed<boolean>;
pendingNotification: ko.Observable<DataModels.Notification>;
conflictResolutionPolicyMode: ko.Observable<string>;
conflictResolutionPolicyPath: ko.Observable<string>;
conflictResolutionPolicyProcedure: ko.Observable<string>;
rupmVisible: ko.Computed<boolean>;
costsVisible: ko.Computed<boolean>;
minRUAnotationVisible: ko.Computed<boolean>;
/* Command Bar */
saveSettingsButton: Button;
discardSettingsChangesButton: Button;
onSaveClick(): Q.Promise<any>;
onRevertClick(): Q.Promise<any>;
/* Indexing Policy Editor */
isIndexingPolicyEditorInitializing: ko.Observable<boolean>;
indexingPolicyEditor: ko.Observable<monaco.editor.IStandaloneCodeEditor>;
onValidIndexingPolicyEdit(content: any): Q.Promise<any>;
onInvalidIndexingPolicyEdit(content: any): Q.Promise<any>;
onSaveClick(): Q.Promise<any>;
onRevertClick(): Q.Promise<any>;
}
export interface DatabaseSettingsTab extends Tab {
/*state*/
throughput: ko.Observable<number>;
requestUnitsUsageCost: ko.PureComputed<string>;
canThroughputExceedMaximumValue: ko.Computed<boolean>;
warningMessage: ko.Computed<string>;
notificationStatusInfo: ko.Observable<string>;
shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
shouldShowStatusBar: ko.Computed<boolean>;
pendingNotification: ko.Observable<DataModels.Notification>;
costsVisible: ko.Computed<boolean>;
minRUAnotationVisible: ko.Computed<boolean>;
/* Command Bar */
saveSettingsButton: Button;
discardSettingsChangesButton: Button;
onSaveClick(): Q.Promise<any>;
onRevertClick(): Q.Promise<any>;
/* Errors */
displayedError: ko.Observable<string>;
onSaveClick(): Q.Promise<any>;
onRevertClick(): Q.Promise<any>;
}
export interface WaitsForTemplate {
isTemplateReady: ko.Observable<boolean>;
}
export interface QueryTab extends Tab {
queryEditorId: string;
isQueryMetricsEnabled: ko.Computed<boolean>;
activityId: ko.Observable<string>;
/* Command Bar */
executeQueryButton: Button;
fetchNextPageButton: Button;
saveQueryButton: Button;
onExecuteQueryClick(): Q.Promise<any>;
onFetchNextPageClick(): Q.Promise<any>;
/*Query Editor*/
initialEditorContent: ko.Observable<string>;
sqlQueryEditorContent: ko.Observable<string>;
sqlStatementToExecute: ko.Observable<string>;
/* Results */
allResultsMetadata: ko.ObservableArray<QueryResultsMetadata>;
/* Errors */
errors: ko.ObservableArray<QueryError>;
/* Status */
statusMessge: ko.Observable<string>;
statusIcon: ko.Observable<string>;
}
export interface ScriptTab extends Tab {
id: Editable<string>;
editorId: string;
saveButton: Button;
updateButton: Button;
discardButton: Button;
deleteButton: Button;
editorState: ko.Observable<ScriptEditorState>;
editorContent: ko.Observable<string>;
editor: ko.Observable<monaco.editor.IStandaloneCodeEditor>;
errors: ko.ObservableArray<QueryError>;
statusMessge: ko.Observable<string>;
statusIcon: ko.Observable<string>;
formFields: ko.ObservableArray<Editable<any>>;
formIsValid: ko.Computed<boolean>;
formIsDirty: ko.Computed<boolean>;
isNew: ko.Observable<boolean>;
resource: ko.Observable<DataModels.Resource>;
setBaselines(): void;
}
export interface StoredProcedureTab extends ScriptTab {
onExecuteSprocsResult(result: any, logsData: any): void;
onExecuteSprocsError(error: string): void;
}
export interface UserDefinedFunctionTab extends ScriptTab {}
export interface TriggerTab extends ScriptTab {
triggerType: Editable<string>;
triggerOperation: Editable<string>;
}
export interface GraphTab extends Tab {}
export interface EditorPosition {
line: number;
column: number;
}
export interface MongoShellTab extends Tab {}
export enum DocumentExplorerState {
noDocumentSelected,
newDocumentValid,
@@ -1282,40 +429,8 @@ export interface AuthorizationTokenHeaderMetadata {
token: string;
}
export interface TelemetryActions {
sendEvent(name: string, telemetryProperties?: { [propertyName: string]: string }): Q.Promise<any>;
sendError(errorInfo: DataModels.ITelemetryError): Q.Promise<any>;
sendMetric(
name: string,
metricNumber: number,
telemetryProperties?: { [propertyName: string]: string }
): Q.Promise<any>;
}
export interface ConfigurationOverrides {
EnableBsonSchema: string;
}
export interface CosmosDbApi {
isSystemDatabasePredicate: (database: Database) => boolean;
}
export interface DropdownOption<T> {
text: string;
value: T;
disable?: boolean;
}
export interface INotebookContainerClient {
resetWorkspace: () => Promise<void>;
}
export interface INotebookContentClient {
updateItemChildren: (item: NotebookContentItem) => Promise<void>;
createNewNotebookFile: (parent: NotebookContentItem) => Promise<NotebookContentItem>;
deleteContentItem: (item: NotebookContentItem) => Promise<void>;
uploadFileAsync: (name: string, content: string, parent: NotebookContentItem) => Promise<NotebookContentItem>;
renameNotebook: (item: NotebookContentItem, targetName: string) => Promise<NotebookContentItem>;
createDirectory: (parent: NotebookContentItem, newDirectoryName: string) => Promise<NotebookContentItem>;
readFileContent: (filePath: string) => Promise<string>;
}

View File

@@ -12,7 +12,7 @@ import {
PortalTheme
} from "./HeatmapDatatypes";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import { MessageHandler } from "../../Common/MessageHandler";
import { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { StyleConstants } from "../../Common/Constants";
import "./Heatmap.less";
@@ -209,7 +209,7 @@ export class Heatmap {
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]);
}
MessageHandler.sendCachedDataMessage(MessageTypes.LogInfo, output);
sendCachedDataMessage(MessageTypes.LogInfo, output);
});
}
}
@@ -266,4 +266,4 @@ export function handleMessage(event: MessageEvent) {
}
window.addEventListener("message", handleMessage, false);
MessageHandler.sendMessage("ready");
sendMessage("ready");

View File

@@ -0,0 +1,8 @@
export enum DefaultAccountExperienceType {
DocumentDB = "DocumentDB",
Graph = "Graph",
MongoDB = "MongoDB",
Table = "Table",
Cassandra = "Cassandra",
ApiForMongoDB = "Azure Cosmos DB for MongoDB API"
}

View File

@@ -123,12 +123,4 @@ describe("Component Registerer", () => {
it("should register throughput-input component", () => {
expect(ko.components.isRegistered("throughput-input")).toBe(true);
});
it("should register library-manage-pane component", () => {
expect(ko.components.isRegistered("library-manage-pane")).toBe(true);
});
it("should register cluster-library-pane component", () => {
expect(ko.components.isRegistered("cluster-library-pane")).toBe(true);
});
});

View File

@@ -13,9 +13,7 @@ import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponen
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
ko.components.register("toolbar", new ToolbarComponent());
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("error-display", new ErrorDisplayComponent());
@@ -78,6 +76,4 @@ ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPa
ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent());
ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

@@ -12,6 +12,10 @@ import AddTriggerIcon from "../../images/AddTrigger.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import Explorer from "./Explorer";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
export interface CollectionContextMenuButtonParams {
databaseId: string;
@@ -26,7 +30,7 @@ export interface DatabaseContextMenuButtonParams {
*/
export class ResourceTreeContextMenuButtonFactory {
public static createDatabaseContextMenu(
container: ViewModels.Explorer,
container: Explorer,
selectedDatabase: ViewModels.Database
): TreeNodeMenuItem[] {
const newCollectionMenuItem: TreeNodeMenuItem = {
@@ -44,7 +48,7 @@ export class ResourceTreeContextMenuButtonFactory {
}
public static createCollectionContextMenuButton(
container: ViewModels.Explorer,
container: Explorer,
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [];
@@ -115,8 +119,8 @@ export class ResourceTreeContextMenuButtonFactory {
}
public static createStoreProcedureContextMenuItems(
container: ViewModels.Explorer,
storedProcedure: ViewModels.StoredProcedure
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];
@@ -131,10 +135,7 @@ export class ResourceTreeContextMenuButtonFactory {
];
}
public static createTriggerContextMenuItems(
container: ViewModels.Explorer,
trigger: ViewModels.Trigger
): TreeNodeMenuItem[] {
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];
}
@@ -149,8 +150,8 @@ export class ResourceTreeContextMenuButtonFactory {
}
public static createUserDefinedFunctionContextMenuItems(
container: ViewModels.Explorer,
userDefinedFunction: ViewModels.UserDefinedFunction
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];

View File

@@ -23,8 +23,5 @@ interface ErrorDisplayParams {
}
class ErrorDisplayViewModel {
private params: ErrorDisplayParams;
public constructor(params: ErrorDisplayParams) {
this.params = params;
}
public constructor(public params: ErrorDisplayParams) {}
}

View File

@@ -49,6 +49,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
{
key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell",
value: "true"
},
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
{
key: "feature.enablefixedcollectionwithsharedthroughput",

View File

@@ -163,8 +163,14 @@ exports[`Feature panel renders all flags 1`] = `
/>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
key="feature.enablecodeofconduct"
label="Enable Code Of Conduct Acknowledgement"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enableLinkInjection"
label="Enable Injecting Notebook Viewer Link into the first cell"
onChange={[Function]}
/>
</Stack>
@@ -172,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow"
horizontalAlign="space-between"
>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablefixedcollectionwithsharedthroughput"

View File

@@ -1,6 +1,5 @@
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "office-ui-fabric-react";
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as Constants from "../../../Common/Constants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { RepoListItem } from "./GitHubReposComponent";
@@ -9,9 +8,10 @@ import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import UrlUtility from "../../../Common/UrlUtility";
import Explorer from "../../Explorer";
export interface AddRepoComponentProps {
container: ViewModels.Explorer;
container: Explorer;
getRepo: (owner: string, repo: string) => Promise<IGitHubRepo>;
pinRepo: (item: RepoListItem) => void;
}

View File

@@ -1,40 +0,0 @@
import * as React from "react";
import { DetailsList, IColumn, SelectionMode } from "office-ui-fabric-react/lib/DetailsList";
import { Library } from "../../../Contracts/DataModels";
export interface ClusterLibraryItem extends Library {
installed: boolean;
}
export interface ClusterLibraryGridProps {
libraryItems: ClusterLibraryItem[];
onInstalledChanged: (libraryName: string, installed: boolean) => void;
}
export function ClusterLibraryGrid(props: ClusterLibraryGridProps): JSX.Element {
const onInstalledChanged = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target;
const libraryName = (target as any).dataset.name;
const checked = (target as any).checked;
return props.onInstalledChanged(libraryName, checked);
};
const columns: IColumn[] = [
{
key: "name",
name: "Name",
fieldName: "name",
minWidth: 150
},
{
key: "installed",
name: "Installed",
minWidth: 100,
onRender: (item: ClusterLibraryItem) => {
return <input type="checkbox" checked={item.installed} onChange={onInstalledChanged} data-name={item.name} />;
}
}
];
return <DetailsList columns={columns} items={props.libraryItems} selectionMode={SelectionMode.none} />;
}

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { ClusterLibraryGrid, ClusterLibraryGridProps } from "./ClusterLibraryGrid";
export class ClusterLibraryGridAdapter implements ReactAdapter {
public parameters: ko.Observable<ClusterLibraryGridProps>;
public renderComponent(): JSX.Element {
return <ClusterLibraryGrid {...this.parameters()} />;
}
}

View File

@@ -1,156 +0,0 @@
import * as React from "react";
import DeleteIcon from "../../../../images/delete.svg";
import { Button } from "office-ui-fabric-react/lib/Button";
import { DetailsList, IColumn, SelectionMode } from "office-ui-fabric-react/lib/DetailsList";
import { Library } from "../../../Contracts/DataModels";
import { Label } from "office-ui-fabric-react/lib/Label";
import { SparkLibrary } from "../../../Common/Constants";
import { TextField } from "office-ui-fabric-react/lib/TextField";
export interface LibraryManageComponentProps {
addProps: {
nameProps: LibraryAddNameTextFieldProps;
urlProps: LibraryAddUrlTextFieldProps;
buttonProps: LibraryAddButtonProps;
};
gridProps: LibraryManageGridProps;
}
export function LibraryManageComponent(props: LibraryManageComponentProps): JSX.Element {
const {
addProps: { nameProps, urlProps, buttonProps },
gridProps
} = props;
return (
<div>
<div className="library-add-container">
<LibraryAddNameTextField {...nameProps} />
<LibraryAddUrlTextField {...urlProps} />
<LibraryAddButton {...buttonProps} />
</div>
<div className="library-grid-container">
<Label>All Libraries</Label>
<LibraryManageGrid {...gridProps} />
</div>
</div>
);
}
export interface LibraryManageGridProps {
items: Library[];
onLibraryDeleteClick: (libraryName: string) => void;
}
function LibraryManageGrid(props: LibraryManageGridProps): JSX.Element {
const columns: IColumn[] = [
{
key: "name",
name: "Name",
fieldName: "name",
minWidth: 200
},
{
key: "delete",
name: "Delete",
minWidth: 60,
onRender: (item: Library) => {
const onDelete = () => {
props.onLibraryDeleteClick(item.name);
};
return (
<span className="library-delete">
<img src={DeleteIcon} alt="Delete" onClick={onDelete} />
</span>
);
}
}
];
return <DetailsList columns={columns} items={props.items} selectionMode={SelectionMode.none} />;
}
export interface LibraryAddButtonProps {
disabled: boolean;
onLibraryAddClick: (event: React.FormEvent<any>) => void;
}
function LibraryAddButton(props: LibraryAddButtonProps): JSX.Element {
return (
<Button text="Add" className="library-add-button" onClick={props.onLibraryAddClick} disabled={props.disabled} />
);
}
export interface LibraryAddUrlTextFieldProps {
libraryAddress: string;
onLibraryAddressChange: (libraryAddress: string) => void;
onLibraryAddressValidated: (errorMessage: string, value: string) => void;
}
function LibraryAddUrlTextField(props: LibraryAddUrlTextFieldProps): JSX.Element {
const handleTextChange = (e: React.FormEvent<any>, libraryAddress: string) => {
props.onLibraryAddressChange(libraryAddress);
};
const validateText = (text: string): string => {
if (!text) {
return "";
}
const libraryUrlRegex = /^(https:\/\/.+\/)(.+)\.(jar)$/gi;
const isValidUrl = libraryUrlRegex.test(text);
if (isValidUrl) {
return "";
}
return "Need to be a valid https uri";
};
return (
<TextField
value={props.libraryAddress}
label="Url"
type="url"
className="library-add-textfield"
onChange={handleTextChange}
onGetErrorMessage={validateText}
onNotifyValidationResult={props.onLibraryAddressValidated}
placeholder="https://myrepo/myjar.jar"
autoComplete="off"
/>
);
}
export interface LibraryAddNameTextFieldProps {
libraryName: string;
onLibraryNameChange: (libraryName: string) => void;
onLibraryNameValidated: (errorMessage: string, value: string) => void;
}
function LibraryAddNameTextField(props: LibraryAddNameTextFieldProps): JSX.Element {
const handleTextChange = (e: React.FormEvent<any>, libraryName: string) => {
props.onLibraryNameChange(libraryName);
};
const validateText = (text: string): string => {
if (!text) {
return "";
}
const length = text.length;
if (length < SparkLibrary.nameMinLength || length > SparkLibrary.nameMaxLength) {
return "Library name length need to be between 3 and 63.";
}
const nameRegex = /^[a-z0-9][-a-z0-9]*[a-z0-9]$/gi;
const isValidUrl = nameRegex.test(text);
if (isValidUrl) {
return "";
}
return "Need to be a valid name. Letters, numbers and - are allowed";
};
return (
<TextField
value={props.libraryName}
label="Name"
type="text"
className="library-add-textfield"
onChange={handleTextChange}
onGetErrorMessage={validateText}
onNotifyValidationResult={props.onLibraryNameValidated}
placeholder="myjar"
autoComplete="off"
/>
);
}

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { LibraryManageComponent, LibraryManageComponentProps } from "./LibraryManage";
export class LibraryManageComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<LibraryManageComponentProps>;
public renderComponent(): JSX.Element {
return <LibraryManageComponent {...this.parameters()} />;
}
}

View File

@@ -5,7 +5,7 @@
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import * as Logger from "../../../Common/Logger";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils";

View File

@@ -17,7 +17,8 @@ describe("GalleryCardComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0
views: 0,
newCellId: undefined
},
isFavorite: false,
showDownload: true,

View File

@@ -36,6 +36,8 @@ export interface GalleryCardComponentProps {
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
public static readonly CARD_WIDTH = 256;
private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio =
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
private static readonly cardDescriptionMaxChars = 88;
private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8;

View File

@@ -0,0 +1,43 @@
import { shallow } from "enzyme";
import * as sinon from "sinon";
import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
import { IJunoResponse, JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes } from "../../../Common/Constants";
describe("CodeOfConductComponent", () => {
let sandbox: sinon.SinonSandbox;
let codeOfConductProps: CodeOfConductComponentProps;
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(JunoClient.prototype, "acceptCodeOfConduct").returns({
status: HttpStatusCodes.OK,
data: true
} as IJunoResponse<boolean>);
const junoClient = new JunoClient(undefined);
codeOfConductProps = {
junoClient: junoClient,
onAcceptCodeOfConduct: jest.fn()
};
});
afterEach(() => {
sandbox.restore();
});
it("renders", () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
expect(wrapper).toMatchSnapshot();
});
it("onAcceptedCodeOfConductCalled", async () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
wrapper
.find(".genericPaneSubmitBtn")
.first()
.simulate("click");
await Promise.resolve();
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
});
});

View File

@@ -0,0 +1,112 @@
import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import * as Logger from "../../../Common/Logger";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void;
}
interface CodeOfConductComponentState {
readCodeOfConduct: boolean;
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
private link2: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
this.state = {
readCodeOfConduct: false
};
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
this.descriptionPara2 =
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
}
private async acceptCodeOfConduct(): Promise<void> {
try {
const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
const message = `Failed to accept code of conduct: ${error}`;
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
logConsoleError(message);
}
}
private onChangeCheckbox = (): void => {
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
};
public render(): JSX.Element {
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{this.descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<Text>
{this.descriptionPara3}
<Link href={this.link1.url} target="_blank">
{this.link1.label}
</Link>
{" and "}
<Link href={this.link2.url} target="_blank">
{this.link2.label}
</Link>
</Text>
</Stack.Item>
<Stack.Item>
<Checkbox
styles={{
label: {
margin: 0,
padding: "2 0 2 0"
},
text: {
fontSize: 12
}
}}
label="I have read and accepted the code of conduct and privacy statement"
onChange={this.onChangeCheckbox}
/>
</Stack.Item>
<Stack.Item>
<PrimaryButton
ariaLabel="Continue"
title="Continue"
onClick={async () => await this.acceptCodeOfConduct()}
tabIndex={0}
className="genericPaneSubmitBtn"
text="Continue"
disabled={!this.state.readCodeOfConduct}
/>
</Stack.Item>
</Stack>
);
}
}

View File

@@ -1,12 +1,12 @@
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient";
import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent";
import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import Explorer from "../../Explorer";
export interface GalleryAndNotebookViewerComponentProps {
container?: ViewModels.Explorer;
container?: Explorer;
junoClient: JunoClient;
notebookUrl?: string;
galleryItem?: IGalleryItem;

View File

@@ -15,18 +15,20 @@ import {
} from "office-ui-fabric-react";
import * as React from "react";
import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less";
import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent";
export interface GalleryViewerComponentProps {
container?: ViewModels.Explorer;
container?: Explorer;
junoClient: JunoClient;
selectedTab: GalleryTab;
sortBy: SortBy;
@@ -60,6 +62,7 @@ interface GalleryViewerComponentState {
sortBy: SortBy;
searchText: string;
dialogProps: DialogProps;
isCodeOfConductAccepted: boolean;
}
interface GalleryTabInfo {
@@ -86,6 +89,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private publicNotebooks: IGalleryItem[];
private favoriteNotebooks: IGalleryItem[];
private publishedNotebooks: IGalleryItem[];
private isCodeOfConductAccepted: boolean;
private columnCount: number;
private rowCount: number;
@@ -100,7 +104,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
selectedTab: props.selectedTab,
sortBy: props.sortBy,
searchText: props.searchText,
dialogProps: undefined
dialogProps: undefined,
isCodeOfConductAccepted: undefined
};
this.sortingOptions = [
@@ -134,9 +139,20 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) {
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
tabs.push(
this.createPublicGalleryTab(
GalleryTab.PublicGallery,
this.state.publicNotebooks,
this.state.isCodeOfConductAccepted
)
);
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior.
if (this.state.isCodeOfConductAccepted !== false) {
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
}
}
const pivotProps: IPivotProps = {
@@ -167,6 +183,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
);
}
private createPublicGalleryTab(
tab: GalleryTab,
data: IGalleryItem[],
acceptedCodeOfConduct: boolean
): GalleryTabInfo {
return {
tab,
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct)
};
}
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return {
tab,
@@ -174,6 +201,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
};
}
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return acceptedCodeOfConduct === false ? (
<CodeOfConductComponent
junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
) : (
this.createTabContent(data)
);
}
private createTabContent(data: IGalleryItem[]): JSX.Element {
return (
<Stack tokens={{ childrenGap: 10 }}>
@@ -187,8 +227,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item>
{this.props.container?.isGalleryPublishEnabled() && (
<Stack.Item>
<InfoComponent />
</Stack.Item>
)}
</Stack>
{data && this.createCardsTabContent(data)}
</Stack>
);
@@ -254,12 +298,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) {
try {
const response = await this.props.junoClient.getPublicNotebooks();
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
if (this.props.container.isCodeOfConductEnabled()) {
response = await this.props.junoClient.fetchPublicNotebooks();
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
this.publicNotebooks = response.data?.notebooksData;
} else {
response = await this.props.junoClient.getPublicNotebooks();
this.publicNotebooks = response.data;
}
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
}
this.publicNotebooks = response.data;
} catch (error) {
const message = `Failed to load public notebooks: ${error}`;
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
@@ -268,7 +319,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
this.setState({
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
isCodeOfConductAccepted: this.isCodeOfConductAccepted
});
}
@@ -333,12 +385,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
const toSearch = searchText.trim().toUpperCase();
const searchData: string[] = [
item.author.toUpperCase(),
item.description.toUpperCase(),
item.name.toUpperCase(),
...item.tags?.map(tag => tag.toUpperCase())
];
const searchData: string[] = [item.author.toUpperCase(), item.description.toUpperCase(), item.name.toUpperCase()];
if (item.tags) {
searchData.push(...item.tags.map(tag => tag.toUpperCase()));
}
for (const data of searchData) {
if (data?.indexOf(toSearch) !== -1) {

View File

@@ -0,0 +1,26 @@
@import "../../../../../less/Common/Constants.less";
.infoPanel, .infoPanelMain {
display: flex;
align-items: center;
}
.infoPanel {
padding-left: 5px;
padding-right: 5px;
}
.infoLabel, .infoLabelMain {
padding-left: 5px
}
.infoLabel {
font-weight: 400
}
.infoIconMain {
color: @AccentMedium
}
.infoIconMain:hover {
color: @BaseMedium
}

View File

@@ -0,0 +1,10 @@
import { shallow } from "enzyme";
import React from "react";
import { InfoComponent } from "./InfoComponent";
describe("InfoComponent", () => {
it("renders", () => {
const wrapper = shallow(<InfoComponent />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,42 @@
import * as React from "react";
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fabric-react";
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
import "./InfoComponent.less";
export class InfoComponent extends React.Component {
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
return (
<Link href={url} target="_blank">
<div className="infoPanel">
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabel">{labelText}</Label>
</div>
</Link>
);
};
private onHover = (): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5, padding: 5 }}>
<Stack.Item>{this.getInfoPanel("Script", "Code of Conduct", CodeOfConductEndpoints.codeOfConduct)}</Stack.Item>
<Stack.Item>
{this.getInfoPanel("RedEye", "Privacy Statement", CodeOfConductEndpoints.privacyStatement)}
</Stack.Item>
<Stack.Item>
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
</Stack.Item>
</Stack>
);
};
public render(): JSX.Element {
return (
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
<div className="infoPanelMain">
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabelMain">Help</Label>
</div>
</HoverCard>
);
}
}

View File

@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InfoComponent renders 1`] = `
<StyledHoverCardBase
instantOpenOnClick={true}
plainCardProps={
Object {
"onRenderPlainCard": [Function],
}
}
type="PlainCard"
>
<div
className="infoPanelMain"
>
<Memo(StyledIconBase)
className="infoIconMain"
iconName="Help"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
<StyledLabelBase
className="infoLabelMain"
>
Help
</StyledLabelBase>
</div>
</StyledHoverCardBase>
`;

View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeOfConductComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StackItem>
<Text
style={
Object {
"fontSize": "20px",
"fontWeight": 500,
}
}
>
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
</Text>
</StackItem>
<StackItem>
<Text>
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
</Text>
</StackItem>
<StackItem>
<Text>
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
<StyledLinkBase
href="https://aka.ms/cosmos-code-of-conduct"
target="_blank"
>
code of conduct
</StyledLinkBase>
and
<StyledLinkBase
href="https://aka.ms/ms-privacy-policy"
target="_blank"
>
privacy statement
</StyledLinkBase>
</Text>
</StackItem>
<StackItem>
<StyledCheckboxBase
label="I have read and accepted the code of conduct and privacy statement"
onChange={[Function]}
styles={
Object {
"label": Object {
"margin": 0,
"padding": "2 0 2 0",
},
"text": Object {
"fontSize": 12,
},
}
}
/>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
ariaLabel="Continue"
className="genericPaneSubmitBtn"
disabled={true}
onClick={[Function]}
tabIndex={0}
text="Continue"
title="Continue"
/>
</StackItem>
</Stack>
`;

View File

@@ -17,7 +17,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0
views: 0,
newCellId: undefined
},
isFavorite: false,
downloadButtonText: "Download",
@@ -45,7 +46,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0
views: 0,
newCellId: undefined
},
isFavorite: true,
downloadButtonText: "Download",

View File

@@ -10,7 +10,7 @@ import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
@@ -18,9 +18,12 @@ import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookRe
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
export interface NotebookViewerComponentProps {
container?: ViewModels.Explorer;
container?: Explorer;
junoClient?: JunoClient;
notebookUrl: string;
galleryItem?: IGalleryItem;
@@ -84,16 +87,17 @@ export class NotebookViewerComponent extends React.Component<
}
const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook);
this.setState({ content: notebook, showProgressBar: false });
if (this.props.galleryItem) {
if (this.props.galleryItem && !SessionStorageUtility.getEntry(this.props.galleryItem.id)) {
const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} while increasing notebook views`);
}
this.setState({ galleryItem: response.data });
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
}
} catch (error) {
this.setState({ showProgressBar: false });
@@ -103,10 +107,21 @@ export class NotebookViewerComponent extends React.Component<
}
}
private removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;
}
const notebookV4 = notebook as NotebookV4;
if (notebookV4 && notebookV4.cells[0].source[0].search(newCellId)) {
delete notebookV4.cells[0];
notebook = notebookV4;
}
};
public render(): JSX.Element {
return (
<div className="notebookViewerContainer">
{this.props.backNavigationText ? (
{this.props.backNavigationText !== undefined ? (
<Link onClick={this.props.onBackClick}>
<Icon iconName="Back" /> {this.props.backNavigationText}
</Link>

View File

@@ -27,9 +27,10 @@ import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/l
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import { QueriesClient } from "../../../Common/QueriesClient";
export interface QueriesGridComponentProps {
queriesClient: ViewModels.QueriesClient;
queriesClient: QueriesClient;
onQuerySelect: (query: DataModels.Query) => void;
containerVisible: boolean;
saveQueryEnabled: boolean;
@@ -217,7 +218,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
menuItem: any
) => {
if (window.confirm("Are you sure you want to delete this query?")) {
const container: ViewModels.Explorer = window.dataExplorer;
const container = window.dataExplorer;
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
databaseAccountName: container && container.databaseAccount().name,
defaultExperience: container && container.defaultExperience(),

View File

@@ -8,11 +8,12 @@ import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { QueriesGridComponent, QueriesGridComponentProps } from "./QueriesGridComponent";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import Explorer from "../../Explorer";
export class QueriesGridComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private container: ViewModels.Explorer) {
constructor(private container: Explorer) {
this.parameters = ko.observable<number>(Date.now());
}

View File

@@ -1,6 +1,6 @@
/* Utilities for validation */
export const onValidateValueChange = (newValue: string, minValue?: number, maxValue?: number): number => {
export const onValidateValueChange = (newValue: string, minValue?: number, maxValue?: number): number | undefined => {
let numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
if (minValue !== undefined && numericValue < minValue) {
@@ -16,7 +16,7 @@ export const onValidateValueChange = (newValue: string, minValue?: number, maxVa
return undefined;
};
export const onIncrementValue = (newValue: string, step: number, max?: number): number => {
export const onIncrementValue = (newValue: string, step: number, max?: number): number | undefined => {
const numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
const newValue = numericValue + step;
@@ -25,7 +25,7 @@ export const onIncrementValue = (newValue: string, step: number, max?: number):
return undefined;
};
export const onDecrementValue = (newValue: string, step: number, min?: number): number => {
export const onDecrementValue = (newValue: string, step: number, min?: number): number | undefined => {
const numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
const newValue = numericValue - step;

View File

@@ -1,17 +0,0 @@
@import "../../../../less/Common/Constants";
.labelWithRedAsterisk {
line-height: 18px;
font-size: @DefaultFontSize;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
.labelWithRedAsterisk::before {
content: "* ";
color: @SelectionHigh;
}
.clusterSettingsDropdown {
margin-bottom: 10px;
}

View File

@@ -1,159 +0,0 @@
import * as React from "react";
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
import { Slider, ISliderProps } from "office-ui-fabric-react/lib/Slider";
import { Stack, IStackItemStyles, IStackStyles } from "office-ui-fabric-react/lib/Stack";
import { TextField, ITextFieldProps } from "office-ui-fabric-react/lib/TextField";
import { Spark } from "../../../Common/Constants";
import { SparkCluster } from "../../../Contracts/DataModels";
export interface ClusterSettingsComponentProps {
cluster: SparkCluster;
onClusterSettingsChanged: (cluster: SparkCluster) => void;
}
export class ClusterSettingsComponent extends React.Component<ClusterSettingsComponentProps, {}> {
constructor(props: ClusterSettingsComponentProps) {
super(props);
}
public render(): JSX.Element {
return (
<>
{this.getMasterSizeDropdown()}
{this.getWorkerSizeDropdown()}
{this.getWorkerCountSliderInput()}
</>
);
}
private getMasterSizeDropdown(): JSX.Element {
const driverSize: string =
this.props.cluster && this.props.cluster.properties && this.props.cluster.properties.driverSize;
const masterSizeOptions: IDropdownOption[] = Spark.SKUs.keys().map(sku => ({
key: sku,
text: Spark.SKUs.get(sku)
}));
const masterSizeDropdownProps: IDropdownProps = {
label: "Master Size",
options: masterSizeOptions,
defaultSelectedKey: driverSize,
onChange: this._onDriverSizeChange,
styles: {
root: "clusterSettingsDropdown"
}
};
return <Dropdown {...masterSizeDropdownProps} />;
}
private getWorkerSizeDropdown(): JSX.Element {
const workerSize: string =
this.props.cluster && this.props.cluster.properties && this.props.cluster.properties.workerSize;
const workerSizeOptions: IDropdownOption[] = Spark.SKUs.keys().map(sku => ({
key: sku,
text: Spark.SKUs.get(sku)
}));
const workerSizeDropdownProps: IDropdownProps = {
label: "Worker Size",
options: workerSizeOptions,
defaultSelectedKey: workerSize,
onChange: this._onWorkerSizeChange,
styles: {
label: "labelWithRedAsterisk",
root: "clusterSettingsDropdown"
}
};
return <Dropdown {...workerSizeDropdownProps} />;
}
private getWorkerCountSliderInput(): JSX.Element {
const workerCount: number =
(this.props.cluster &&
this.props.cluster.properties &&
this.props.cluster.properties.workerInstanceCount !== undefined &&
this.props.cluster.properties.workerInstanceCount) ||
0;
const stackStyle: IStackStyles = {
root: {
paddingTop: 5
}
};
const sliderItemStyle: IStackItemStyles = {
root: {
width: "100%",
paddingRight: 20
}
};
const workerCountSliderProps: ISliderProps = {
min: 0,
max: Spark.MaxWorkerCount,
step: 1,
value: workerCount,
showValue: false,
onChange: this._onWorkerCountChange,
styles: {
root: {
width: "100%",
paddingRight: 20
}
}
};
const workerCountTextFieldProps: ITextFieldProps = {
value: workerCount.toString(),
styles: {
fieldGroup: {
width: 45,
height: 25
},
field: {
textAlign: "center"
}
},
onChange: this._onWorkerCountTextFieldChange
};
return (
<Stack styles={stackStyle}>
<span className="labelWithRedAsterisk">Worker Nodes</span>
<Stack horizontal verticalAlign="center">
<Slider {...workerCountSliderProps} />
<TextField {...workerCountTextFieldProps} />
</Stack>
</Stack>
);
}
private _onDriverSizeChange = (_event: React.FormEvent, selectedOption: IDropdownOption) => {
const newValue: string = selectedOption.key as string;
const cluster = this.props.cluster;
if (cluster) {
cluster.properties.driverSize = newValue;
this.props.onClusterSettingsChanged(cluster);
}
};
private _onWorkerSizeChange = (_event: React.FormEvent, selectedOption: IDropdownOption) => {
const newValue: string = selectedOption.key as string;
const cluster = this.props.cluster;
if (cluster) {
cluster.properties.workerSize = newValue;
this.props.onClusterSettingsChanged(cluster);
}
};
private _onWorkerCountChange = (count: number) => {
count = Math.min(count, Spark.MaxWorkerCount);
const cluster = this.props.cluster;
if (cluster) {
cluster.properties.workerInstanceCount = count;
this.props.onClusterSettingsChanged(cluster);
}
};
private _onWorkerCountTextFieldChange = (_event: React.FormEvent, newValue: string) => {
const count = parseInt(newValue);
if (!isNaN(count)) {
this._onWorkerCountChange(count);
}
};
}

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ClusterSettingsComponent, ClusterSettingsComponentProps } from "./ClusterSettingsComponent";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
export class ClusterSettingsComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<ClusterSettingsComponentProps>;
public renderComponent(): JSX.Element {
return <ClusterSettingsComponent {...this.parameters()} />;
}
}

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarAction extends IToolbarDisplayable {
type: "action";
action: () => void;
}
export default IToolbarAction;

View File

@@ -1,18 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
interface IToolbarDisplayable {
id: string;
title: ko.Subscribable<string>;
displayName: ko.Subscribable<string>;
enabled: ko.Subscribable<boolean>;
visible: ko.Observable<boolean>;
focused: ko.Observable<boolean>;
icon: string;
mouseDown: (data: any, event: MouseEvent) => any;
keyUp: (data: any, event: KeyboardEvent) => any;
keyDown: (data: any, event: KeyboardEvent) => any;
}
export default IToolbarDisplayable;

View File

@@ -1,56 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarDropDown extends IToolbarDisplayable {
type: "dropdown";
subgroup: IActionConfigItem[];
expanded: ko.Observable<boolean>;
open: () => void;
}
export interface IDropdown {
type: "dropdown";
title: string;
displayName: string;
id: string;
enabled: ko.Observable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
subgroup?: IActionConfigItem[];
}
export interface ISeperator {
type: "separator";
visible?: ko.Observable<boolean>;
}
export interface IToggle {
type: "toggle";
title: string;
displayName: string;
checkedTitle: string;
checkedDisplayName: string;
id: string;
checked: ko.Observable<boolean>;
enabled: ko.Observable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
}
export interface IAction {
type: "action";
title: string;
displayName: string;
id: string;
action: () => any;
enabled: ko.Subscribable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
}
export type IActionConfigItem = ISeperator | IAction | IToggle | IDropdown;
export default IToolbarDropDown;

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarAction from "./IToolbarAction";
import IToolbarToggle from "./IToolbarToggle";
import IToolbarSeperator from "./IToolbarSeperator";
import IToolbarDropDown from "./IToolbarDropDown";
type IToolbarItem = IToolbarAction | IToolbarToggle | IToolbarSeperator | IToolbarDropDown;
export default IToolbarItem;

View File

@@ -1,10 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
interface IToolbarSeperator {
type: "separator";
visible: ko.Observable<boolean>;
}
export default IToolbarSeperator;

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarToggle extends IToolbarDisplayable {
type: "toggle";
checked: ko.Observable<boolean>;
toggle: () => void;
}
export default IToolbarToggle;

View File

@@ -1,58 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
var keyCodes = {
RightClick: 3,
Enter: 13,
Esc: 27,
Tab: 9,
LeftArrow: 37,
UpArrow: 38,
RightArrow: 39,
DownArrow: 40,
Delete: 46,
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
Period: 190,
DecimalPoint: 110,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
Dash: 189
};
export default keyCodes;

View File

@@ -1,145 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import { IDropdown } from "./IToolbarDropDown";
import { IActionConfigItem } from "./IToolbarDropDown";
import IToolbarItem from "./IToolbarItem";
import * as ko from "knockout";
import ToolbarDropDown from "./ToolbarDropDown";
import ToolbarAction from "./ToolbarAction";
import ToolbarToggle from "./ToolbarToggle";
import template from "./toolbar.html";
export default class Toolbar {
private _toolbarWidth = ko.observable<number>();
private _actionConfigs: IActionConfigItem[];
private _afterExecute: (id: string) => void;
private _hasFocus: boolean = false;
private _focusedSubscription: ko.Subscription;
constructor(actionItems: IActionConfigItem[], afterExecute?: (id: string) => void) {
this._actionConfigs = actionItems;
this._afterExecute = afterExecute;
this.toolbarItems.subscribe(this._focusFirstEnabledItem);
$(window).resize(() => {
this._toolbarWidth($(".toolbar").width());
});
setTimeout(() => {
this._toolbarWidth($(".toolbar").width());
}, 500);
}
public toolbarItems: ko.PureComputed<IToolbarItem[]> = ko.pureComputed(() => {
var remainingToolbarSpace = this._toolbarWidth();
var toolbarItems: IToolbarItem[] = [];
var moreItem: IDropdown = {
type: "dropdown",
title: "More",
displayName: "More",
id: "more-actions-toggle",
enabled: ko.observable(true),
visible: ko.observable(true),
icon: "images/ASX_More.svg",
subgroup: []
};
var showHasMoreItem = false;
var addSeparator = false;
this._actionConfigs.forEach(actionConfig => {
if (actionConfig.type === "separator") {
addSeparator = true;
} else if (remainingToolbarSpace / 60 > 2) {
if (addSeparator) {
addSeparator = false;
toolbarItems.push(Toolbar._createToolbarItemFromConfig({ type: "separator" }));
remainingToolbarSpace -= 10;
}
toolbarItems.push(Toolbar._createToolbarItemFromConfig(actionConfig));
remainingToolbarSpace -= 60;
} else {
showHasMoreItem = true;
if (addSeparator) {
addSeparator = false;
moreItem.subgroup.push({
type: "separator"
});
}
if (!!actionConfig) {
moreItem.subgroup.push(actionConfig);
}
}
});
if (showHasMoreItem) {
toolbarItems.push(
Toolbar._createToolbarItemFromConfig({ type: "separator" }),
Toolbar._createToolbarItemFromConfig(moreItem)
);
}
return toolbarItems;
});
public focus() {
this._hasFocus = true;
this._focusFirstEnabledItem(this.toolbarItems());
}
private _focusFirstEnabledItem = (items: IToolbarItem[]) => {
if (!!this._focusedSubscription) {
// no memory leaks! :D
this._focusedSubscription.dispose();
}
if (this._hasFocus) {
for (var i = 0; i < items.length; i++) {
if (items[i].type !== "separator" && (<any>items[i]).enabled()) {
(<any>items[i]).focused(true);
this._focusedSubscription = (<any>items[i]).focused.subscribe((newValue: any) => {
if (!newValue) {
this._hasFocus = false;
this._focusedSubscription.dispose();
}
});
break;
}
}
}
};
private static _createToolbarItemFromConfig(
configItem: IActionConfigItem,
afterExecute?: (id: string) => void
): IToolbarItem {
switch (configItem.type) {
case "dropdown":
return new ToolbarDropDown(configItem, afterExecute);
case "action":
return new ToolbarAction(configItem, afterExecute);
case "toggle":
return new ToolbarToggle(configItem, afterExecute);
case "separator":
return {
type: "separator",
visible: ko.observable(true)
};
}
}
}
/**
* Helper class for ko component registration
*/
export class ToolbarComponent {
constructor() {
return {
viewModel: Toolbar,
template
};
}
}

View File

@@ -1,86 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IAction } from "./IToolbarDropDown";
import IToolbarAction from "./IToolbarAction";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
export default class ToolbarAction implements IToolbarAction {
public type: "action" = "action";
public id: string;
public icon: string;
public title: ko.Observable<string>;
public displayName: ko.Observable<string>;
public enabled: ko.Subscribable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public action: () => void;
private _afterExecute: (id: string) => void;
constructor(actionItem: IAction, afterExecute?: (id: string) => void) {
this.action = actionItem.action;
this.title = ko.observable(actionItem.title);
this.displayName = ko.observable(actionItem.displayName);
this.id = actionItem.id;
this.enabled = actionItem.enabled;
this.visible = actionItem.visible ? actionItem.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = actionItem.icon;
this._afterExecute = afterExecute;
}
private _executeAction = () => {
this.action();
if (!!this._afterExecute) {
this._afterExecute(this.id);
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this._executeAction();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this._executeAction();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,167 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IDropdown } from "./IToolbarDropDown";
import { IActionConfigItem } from "./IToolbarDropDown";
import IToolbarDropDown from "./IToolbarDropDown";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
interface IMenuItem {
id?: string;
type: "normal" | "separator" | "submenu";
label?: string;
enabled?: boolean;
visible?: boolean;
submenu?: IMenuItem[];
}
export default class ToolbarDropDown implements IToolbarDropDown {
public type: "dropdown" = "dropdown";
public title: ko.Observable<string>;
public displayName: ko.Observable<string>;
public id: string;
public enabled: ko.Observable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public icon: string;
public subgroup: IActionConfigItem[] = [];
public expanded: ko.Observable<boolean> = ko.observable(false);
private _afterExecute: (id: string) => void;
constructor(dropdown: IDropdown, afterExecute?: (id: string) => void) {
this.subgroup = dropdown.subgroup;
this.title = ko.observable(dropdown.title);
this.displayName = ko.observable(dropdown.displayName);
this.id = dropdown.id;
this.enabled = dropdown.enabled;
this.visible = dropdown.visible ? dropdown.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = dropdown.icon;
this._afterExecute = afterExecute;
}
private static _convertToMenuItem = (
actionConfigs: IActionConfigItem[],
actionMap: { [id: string]: () => void } = {}
): { menuItems: IMenuItem[]; actionMap: { [id: string]: () => void } } => {
var returnValue = {
menuItems: actionConfigs.map<IMenuItem>((actionConfig: IActionConfigItem, index, array) => {
var menuItem: IMenuItem;
switch (actionConfig.type) {
case "action":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "normal",
label: actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true
};
actionMap[actionConfig.id] = actionConfig.action;
break;
case "dropdown":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "submenu",
label: actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true,
submenu: ToolbarDropDown._convertToMenuItem(actionConfig.subgroup, actionMap).menuItems
};
break;
case "toggle":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "normal",
label: actionConfig.checked() ? actionConfig.checkedDisplayName : actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true
};
actionMap[actionConfig.id] = () => {
actionConfig.checked(!actionConfig.checked());
};
break;
case "separator":
menuItem = <IMenuItem>{
type: "separator",
visible: true
};
break;
}
return menuItem;
}),
actionMap: actionMap
};
return returnValue;
};
public open = () => {
if (!!(<any>window).host) {
var convertedMenuItem = ToolbarDropDown._convertToMenuItem(this.subgroup);
(<any>window).host
.executeProviderOperation("MenuManager.showMenu", {
iFrameStack: [`#${window.frameElement.id}`],
anchor: `#${this.id}`,
menuItems: convertedMenuItem.menuItems
})
.then((id?: string) => {
if (!!id && !!convertedMenuItem.actionMap[id]) {
convertedMenuItem.actionMap[id]();
}
});
if (!!this._afterExecute) {
this._afterExecute(this.id);
}
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this.open();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this.open();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,109 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IToggle } from "./IToolbarDropDown";
import IToolbarToggle from "./IToolbarToggle";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
export default class ToolbarToggle implements IToolbarToggle {
public type: "toggle" = "toggle";
public checked: ko.Observable<boolean>;
public id: string;
public enabled: ko.Observable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public icon: string;
private _title: string;
private _displayName: string;
private _checkedTitle: string;
private _checkedDisplayName: string;
private _afterExecute: (id: string) => void;
constructor(toggleItem: IToggle, afterExecute?: (id: string) => void) {
this._title = toggleItem.title;
this._displayName = toggleItem.displayName;
this.id = toggleItem.id;
this.enabled = toggleItem.enabled;
this.visible = toggleItem.visible ? toggleItem.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = toggleItem.icon;
this.checked = toggleItem.checked;
this._checkedTitle = toggleItem.checkedTitle;
this._checkedDisplayName = toggleItem.checkedDisplayName;
this._afterExecute = afterExecute;
}
public title = ko.pureComputed(() => {
if (this.checked()) {
return this._checkedTitle;
} else {
return this._title;
}
});
public displayName = ko.pureComputed(() => {
if (this.checked()) {
return this._checkedDisplayName;
} else {
return this._displayName;
}
});
public toggle = () => {
this.checked(!this.checked());
if (this.checked() && !!this._afterExecute) {
this._afterExecute(this.id);
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this.toggle();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this.toggle();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,166 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import KeyCodes from "./KeyCodes";
export default class Utilities {
/**
* Executes an action on a keyboard event.
* Modifiers: ctrlKey - control/command key, shiftKey - shift key, altKey - alt/option key;
* pass on 'null' to ignore the modifier (default).
*/
public static onKey(
event: any,
eventKeyCode: number,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
var source: any = event.target || event.srcElement,
keyCode: number = event.keyCode,
$sourceElement = $(source),
handled: boolean = false;
if (
$sourceElement.length &&
keyCode === eventKeyCode &&
$.isFunction(action) &&
(metaKey === null || metaKey === event.metaKey) &&
(shiftKey === null || shiftKey === event.shiftKey) &&
(altKey === null || altKey === event.altKey)
) {
action($sourceElement);
handled = true;
}
return handled;
}
/**
* Executes an action on the first matched keyboard event.
*/
public static onKeys(
event: any,
eventKeyCodes: number[],
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
var handled: boolean = false,
keyCount: number,
i: number;
if ($.isArray(eventKeyCodes)) {
keyCount = eventKeyCodes.length;
for (i = 0; i < keyCount; ++i) {
handled = Utilities.onKey(event, eventKeyCodes[i], action, metaKey, shiftKey, altKey);
if (handled) {
break;
}
}
}
return handled;
}
/**
* Executes an action on an 'enter' keyboard event.
*/
public static onEnter(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Enter, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a 'tab' keyboard event.
*/
public static onTab(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Tab, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on an 'Esc' keyboard event.
*/
public static onEsc(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Esc, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on an 'UpArrow' keyboard event.
*/
public static onUpArrow(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.UpArrow, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a 'DownArrow' keyboard event.
*/
public static onDownArrow(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.DownArrow, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a mouse event.
*/
public static onButton(event: any, eventButtonCode: number, action: ($sourceElement: JQuery) => void): boolean {
var source: any = event.currentTarget;
var buttonCode: number = event.button;
var $sourceElement = $(source);
var handled: boolean = false;
if ($sourceElement.length && buttonCode === eventButtonCode && $.isFunction(action)) {
action($sourceElement);
handled = true;
}
return handled;
}
/**
* Executes an action on a 'left' mouse event.
*/
public static onLeftButton(event: any, action: ($sourceElement: JQuery) => void): boolean {
return Utilities.onButton(event, buttonCodes.Left, action);
}
}
var buttonCodes = {
None: -1,
Left: 0,
Middle: 1,
Right: 2
};

View File

@@ -1,44 +0,0 @@
<div class="toolbar">
<!-- ko template: { name: 'toolbarItemTemplate', foreach: toolbarItems } -->
<!-- /ko -->
</div>
<script type="text/html" id="toolbarItemTemplate">
<!-- ko if: type === "action" -->
<div class="toolbar-group" data-bind="visible: visible">
<button class="toolbar-group-button" data-bind="hasFocus: focused, attr: {id: id, title: title, 'aria-label': displayName}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
<!-- /ko -->
<!-- ko if: type === "toggle" -->
<div class="toolbar-group" data-bind="visible: visible">
<button class="toolbar-group-button toggle-button" data-bind="hasFocus: focused, attr: {id: id, title: title}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon" data-bind="css: { 'toggle-checked': checked }">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
<!-- /ko -->
<!-- ko if: type === "dropdown" -->
<div class="toolbar-group" data-bind="visible: visible">
<div class="dropdown" data-bind="attr: {id: (id + '-dropdown')}">
<button role="menu" class="toolbar-group-button" data-bind="hasFocus: focused, attr: {id: id, title: title, 'aria-label': displayName}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
</div>
<!-- /ko -->
<!-- ko if: type === "separator" -->
<div class="toolbar-group vertical-separator" data-bind="visible: visible"></div>
<!-- /ko -->
</script>

View File

@@ -1,16 +1,17 @@
jest.mock("../../Common/DocumentClientUtilityBase");
import * as ko from "knockout";
import * as sinon from "sinon";
import * as ViewModels from "../../Contracts/ViewModels";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import Q from "q";
import { CollectionStub, DatabaseStub, ExplorerStub } from "../OpenActionsStubs";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { CosmosClient } from "../../Common/CosmosClient";
import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext";
describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): ExplorerStub => {
const explorerStub = new ExplorerStub();
const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer;
explorerStub.nonSystemDatabases = ko.computed(() => [database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
@@ -53,36 +54,42 @@ describe("ContainerSampleGenerator", () => {
}
]
};
const collection = new CollectionStub({ id: ko.observable(sampleCollectionId) });
const database = new DatabaseStub({
const collection = { id: ko.observable(sampleCollectionId) } as ViewModels.Collection;
const database = {
id: ko.observable(sampleDatabaseId),
collections: ko.observableArray([collection])
});
collections: ko.observableArray<ViewModels.Collection>([collection])
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true);
const fakeDocumentClientUtility = sinon.createStubInstance(DocumentClientUtilityBase);
fakeDocumentClientUtility.getOrCreateDatabaseAndCollection.returns(Q.resolve(collection));
fakeDocumentClientUtility.createDocument.returns(Q.resolve());
explorerStub.documentClientUtility = fakeDocumentClientUtility;
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
await generator.createSampleContainerAsync();
expect(fakeDocumentClientUtility.createDocument.called).toBe(true);
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
});
it("should send gremlin queries for Graph API account", async () => {
sinon.stub(GremlinClient.prototype, "initialize").callsFake(() => {});
const executeStub = sinon.stub(GremlinClient.prototype, "execute").returns(Q.resolve());
sinon.stub(CosmosClient, "databaseAccount").returns({
properties: {}
updateUserContext({
databaseAccount: {
id: "foo",
name: "foo",
location: "foo",
type: "foo",
kind: "foo",
tags: [],
properties: {
documentEndpoint: "bar",
gremlinEndpoint: "foo",
tableEndpoint: "foo",
cassandraEndpoint: "foo"
}
}
});
const sampleCollectionId = "SampleCollection";
@@ -98,29 +105,23 @@ describe("ContainerSampleGenerator", () => {
"g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)"
]
};
const collection = new CollectionStub({ id: ko.observable(sampleCollectionId) });
const database = new DatabaseStub({
const collection = { id: ko.observable(sampleCollectionId) } as ViewModels.Collection;
const database = {
id: ko.observable(sampleDatabaseId),
collections: ko.observableArray([collection])
});
collections: ko.observableArray<ViewModels.Collection>([collection])
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
collection.databaseId = database.id();
const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true);
const fakeDocumentClientUtility = sinon.createStubInstance(DocumentClientUtilityBase);
fakeDocumentClientUtility.getOrCreateDatabaseAndCollection.returns(Q.resolve(collection));
fakeDocumentClientUtility.createDocument.returns(Q.resolve());
explorerStub.documentClientUtility = fakeDocumentClientUtility;
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
await generator.createSampleContainerAsync();
expect(fakeDocumentClientUtility.createDocument.called).toBe(false);
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
expect(executeStub.called).toBe(true);
});

View File

@@ -3,9 +3,11 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import GraphTab from ".././Tabs/GraphTab";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "../../Common/CosmosClient";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { createDocument, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
data: any[];
@@ -14,12 +16,12 @@ interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
export class ContainerSampleGenerator {
private sampleDataFile: SampleDataFile;
private constructor(private container: ViewModels.Explorer) {}
private constructor(private container: Explorer) {}
/**
* Factory function to load the json data file
*/
public static async createSampleGeneratorAsync(container: ViewModels.Explorer): Promise<ContainerSampleGenerator> {
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
const generator = new ContainerSampleGenerator(container);
let dataFileContent: any;
if (container.isPreferredApiGraph()) {
@@ -63,7 +65,7 @@ export class ContainerSampleGenerator {
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
}
await this.container.documentClientUtility.getOrCreateDatabaseAndCollection(createRequest, options);
await getOrCreateDatabaseAndCollection(createRequest, options);
await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) {
@@ -85,14 +87,14 @@ export class ContainerSampleGenerator {
if (!queries || queries.length < 1) {
return;
}
const account = CosmosClient.databaseAccount();
const account = userContext.databaseAccount;
const databaseId = collection.databaseId;
const gremlinClient = new GremlinClient();
gremlinClient.initialize({
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
databaseId: databaseId,
collectionId: collection.id(),
masterKey: CosmosClient.masterKey() || "",
masterKey: userContext.masterKey || "",
maxResultSize: 100
});
@@ -102,7 +104,7 @@ export class ContainerSampleGenerator {
} else {
// For SQL all queries are executed at the same time
this.sampleDataFile.data.forEach(doc => {
const subPromise = this.container.documentClientUtility.createDocument(collection, doc);
const subPromise = createDocument(collection, doc);
subPromise.catch(reason => NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, reason));
promises.push(subPromise);
});

View File

@@ -1,20 +1,21 @@
import { ExplorerStub, DatabaseStub, CollectionStub } from "../OpenActionsStubs";
import { DataSamplesUtil } from "./DataSamplesUtil";
import * as sinon from "sinon";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as ko from "knockout";
import Explorer from "../Explorer";
import { Database, Collection } from "../../Contracts/ViewModels";
describe("DataSampleUtils", () => {
const sampleCollectionId = "sampleCollectionId";
const sampleDatabaseId = "sampleDatabaseId";
it("should not create sample collection if collection already exists", async () => {
const collection = new CollectionStub({ id: ko.observable(sampleCollectionId) });
const database = new DatabaseStub({
const collection = { id: ko.observable(sampleCollectionId) } as Collection;
const database = {
id: ko.observable(sampleDatabaseId),
collections: ko.observableArray([collection])
});
const explorer = new ExplorerStub();
collections: ko.observableArray<Collection>([collection])
} as Database;
const explorer = {} as Explorer;
explorer.nonSystemDatabases = ko.computed(() => [database]);
explorer.showOkModalDialog = () => {};
const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,11 +1,12 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import Explorer from "../Explorer";
export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container";
constructor(private container: ViewModels.Explorer) {}
constructor(private container: Explorer) {}
/**
* Check if Database/Container is already there: if so, show modal to delete

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ import { GraphData, D3Node, D3Link } from "./GraphData";
import { HashMap } from "../../../Common/HashMap";
import { BaseType } from "d3";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab";
import { GraphExplorer } from "./GraphExplorer";
import * as Constants from "../../../Common/Constants";

View File

@@ -1,3 +1,4 @@
jest.mock("../../../Common/DocumentClientUtilityBase");
import React from "react";
import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme";
@@ -7,11 +8,11 @@ import { GraphExplorer, GraphExplorerProps, GraphAccessor, GraphHighlightedNodeD
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => {
@@ -86,13 +87,31 @@ describe("getPkIdFromDocumentId", () => {
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
});
it("should create pkid pair from partitioned graph (pk as number)", () => {
const doc = createFakeDoc({ id: "id", mypk: 234 });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[234, 'id']");
});
it("should create pkid pair from partitioned graph (pk as boolean)", () => {
const doc = createFakeDoc({ id: "id", mypk: true });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[true, 'id']");
});
it("should create pkid pair from partitioned graph (pk as valid array value)", () => {
const doc = createFakeDoc({ id: "id", mypk: [{ id: "someid", _value: "pkvalue" }] });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
});
it("should error if id is not a string", () => {
const doc = createFakeDoc({ id: { foo: 1 } });
it("should error if id is not a string or number", () => {
let doc = createFakeDoc({ id: { foo: 1 } });
try {
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
expect(true).toBe(false);
} catch (e) {
expect(true).toBe(true);
}
doc = createFakeDoc({ id: true });
try {
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
expect(true).toBe(false);
@@ -101,16 +120,8 @@ describe("getPkIdFromDocumentId", () => {
}
});
it("should error if pk not string nor non-empty array", () => {
let doc = createFakeDoc({ mypk: { foo: 1 } });
try {
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
} catch (e) {
expect(true).toBe(true);
}
doc = createFakeDoc({ mypk: [] });
it("should error if pk is empty array", () => {
let doc = createFakeDoc({ mypk: [] });
try {
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
expect(true).toBe(false);
@@ -134,7 +145,7 @@ describe("GraphExplorer", () => {
const COLLECTION_SELF_LINK = "collectionSelfLink";
const gremlinRU = 789.12;
const createMockProps = (documentClientUtility?: any): GraphExplorerProps => {
const createMockProps = (): GraphExplorerProps => {
const graphConfig = GraphTab.createGraphConfig();
const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig);
@@ -149,7 +160,6 @@ describe("GraphExplorer", () => {
onIsValidQueryChange: (isValidQuery: boolean): void => {},
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
documentClientUtility: documentClientUtility,
collectionRid: COLLECTION_RID,
collectionSelfLink: COLLECTION_SELF_LINK,
graphBackendEndpoint: "graphBackendEndpoint",
@@ -188,7 +198,6 @@ describe("GraphExplorer", () => {
let wrapper: ReactWrapper;
let connectStub: sinon.SinonSpy;
let queryDocStub: sinon.SinonSpy;
let submitToBackendSpy: sinon.SinonSpy;
let renderResultAsJsonStub: sinon.SinonSpy;
let onMiddlePaneInitializedStub: sinon.SinonSpy;
@@ -215,46 +224,6 @@ describe("GraphExplorer", () => {
[query: string]: AjaxResponse;
}
const createDocumentClientUtilityMock = (docDBResponse: AjaxResponse) => {
const mock = {
queryDocuments: () => {},
queryDocumentsPage: (
rid: string,
iterator: any,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> => {
const qresult = {
hasMoreResults: false,
firstItemIndex: firstItemIndex,
lastItemIndex: 0,
itemCount: 0,
documents: docDBResponse.response,
activityId: "",
headers: [] as any[],
requestCharge: gVRU
};
return Q.resolve(qresult);
}
};
const fakeIterator: any = {
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
};
queryDocStub = sinon.stub(mock, "queryDocuments").callsFake(
(container: ViewModels.DocumentRequestContainer, query: string, options: any): Q.Promise<any> => {
(fakeIterator as any)._query = query;
return Q.resolve(fakeIterator);
}
);
return mock;
};
const setupMocks = (
graphExplorer: GraphExplorer,
backendResponses: BackendResponses,
@@ -333,7 +302,29 @@ describe("GraphExplorer", () => {
done: any,
ignoreD3Update: boolean
): GraphExplorer => {
const props: GraphExplorerProps = createMockProps(createDocumentClientUtilityMock(docDBResponse));
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
return Q.resolve({
_query: query,
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
});
});
(queryDocumentsPage as jest.Mock).mockImplementation(
(rid: string, iterator: any, firstItemIndex: number, options: any) => {
return Q.resolve({
hasMoreResults: false,
firstItemIndex: firstItemIndex,
lastItemIndex: 0,
itemCount: 0,
documents: docDBResponse.response,
activityId: "",
headers: [] as any[],
requestCharge: gVRU
});
}
);
const props: GraphExplorerProps = createMockProps();
wrapper = mount(<GraphExplorer {...props} />);
graphExplorerInstance = wrapper.instance() as GraphExplorer;
setupMocks(graphExplorerInstance, backendResponses, done, ignoreD3Update);
@@ -341,7 +332,7 @@ describe("GraphExplorer", () => {
};
const cleanUpStubsWrapper = () => {
queryDocStub.restore();
jest.resetAllMocks();
connectStub.restore();
submitToBackendSpy.restore();
renderResultAsJsonStub.restore();
@@ -378,22 +369,11 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
});
it("should submit g.V() as docdb query with proper query", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[2]
).toBe(DOCDB_G_DOT_V_QUERY);
});
it("should submit g.V() as docdb query with proper parameters", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0]
).toEqual("databaseId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[1]
).toEqual("collectionId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[3]
).toEqual({ maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, enableCrossPartitionQuery: true });
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: true
});
});
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {
@@ -426,22 +406,11 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
});
it("should submit g.V() as docdb query with proper query", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[2]
).toBe(DOCDB_G_DOT_V_QUERY);
});
it("should submit g.V() as docdb query with proper parameters", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0]
).toEqual("databaseId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[1]
).toEqual("collectionId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[3]
).toEqual({ maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, enableCrossPartitionQuery: true });
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: true
});
});
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {

View File

@@ -8,7 +8,7 @@ import * as D3ForceGraph from "./D3ForceGraph";
import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GraphData from "./GraphData";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphUtil } from "./GraphUtil";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
@@ -28,7 +28,7 @@ import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
export interface GraphAccessor {
applyFilter: () => void;
@@ -47,7 +47,6 @@ export interface GraphExplorerProps {
onIsValidQueryChange: (isValidQuery: boolean) => void;
collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
@@ -697,7 +696,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param cmd
*/
public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> {
console.log("submit:", cmd);
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`);
this.setExecuteCounter(this.executeCounter + 1);
@@ -730,26 +728,24 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
*/
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
return this.props.documentClientUtility
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.PAGE_ALL,
enableCrossPartitionQuery:
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
"true"
})
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
return iterator.fetchNext().then(response => response.resources);
},
(reason: any) => {
GraphExplorer.reportToConsole(
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${reason}`,
reason
);
return null;
}
);
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.PAGE_ALL,
enableCrossPartitionQuery:
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
"true"
}).then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
return iterator.fetchNext().then(response => response.resources);
},
(reason: any) => {
GraphExplorer.reportToConsole(
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${reason}`,
reason
);
return null;
}
);
}
/**
@@ -1375,7 +1371,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
let pk = (d as any)[collectionPartitionKeyProperty];
if (typeof pk !== "string") {
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
if (Array.isArray(pk) && pk.length > 0) {
// pk is [{ id: 'id', _value: 'value' }]
pk = pk[0]["_value"];
@@ -1732,12 +1728,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`;
}
return this.props.documentClientUtility
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
})
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
})
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
this.currentDocDBQueryInfo = {
@@ -1766,16 +1760,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return this.props.documentClientUtility
.queryDocumentsPage(
this.props.collectionRid,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,
{
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
}
)
return queryDocumentsPage(
this.props.collectionRid,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,
{
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
}
)
.then((results: ViewModels.QueryResults) => {
GraphExplorer.clearConsoleProgress(id);
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;

View File

@@ -3,7 +3,6 @@ import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { GraphConfig } from "../../Tabs/GraphTab";
import * as ViewModels from "../../../Contracts/ViewModels";
import { GraphExplorer, GraphAccessor } from "./GraphExplorer";
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
interface Parameter {
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
@@ -18,7 +17,6 @@ interface Parameter {
graphConfig?: GraphConfig;
collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
@@ -51,7 +49,6 @@ export class GraphExplorerAdapter implements ReactAdapter {
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
documentClientUtility={this.params.documentClientUtility}
collectionRid={this.params.collectionRid}
collectionSelfLink={this.params.collectionSelfLink}
graphBackendEndpoint={this.params.graphBackendEndpoint}

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