Compare commits

..

14 Commits

Author SHA1 Message Date
Steve Faulkner
6870bd9b54 Tweaks 2020-07-23 21:53:46 -05:00
Steve Faulkner
8aeff8fb45 Tweaks 2020-07-23 21:52:05 -05:00
Steve Faulkner
ee6f635458 Tweak 2020-07-23 19:03:43 -05:00
Steve Faulkner
ad115a2cce Functional version 2020-07-23 19:02:09 -05:00
Steve Faulkner
0c255a55c8 Fix strict 2020-07-23 18:40:55 -05:00
Steve Faulkner
08e84d93b5 Delete -> destory 2020-07-23 18:20:16 -05:00
Steve Faulkner
e2895b62b4 More updates 2020-07-23 18:18:58 -05:00
Steve Faulkner
155aacdf63 Sanitize usage of delete 2020-07-23 18:17:30 -05:00
Steve Faulkner
f4f2d00d7f More tweaks 2020-07-23 18:12:58 -05:00
Steve Faulkner
f1812077e9 Split up generators 2020-07-23 16:35:05 -05:00
Steve Faulkner
cfe9bd8303 More updates 2020-07-23 11:17:20 -05:00
Steve Faulkner
9db8d11801 Setup Namespaces 2020-07-22 22:40:29 -05:00
Steve Faulkner
769a2e7d1c more tweaks 2020-07-22 21:56:45 -05:00
Steve Faulkner
df544f88b2 First pass at a generated ARM client 2020-07-22 21:37:13 -05:00
256 changed files with 14687 additions and 8233 deletions

View File

@@ -298,9 +298,11 @@ 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

View File

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

2
.github/CODEOWNERS vendored
View File

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

View File

@@ -1,13 +1,9 @@
name: CI
on:
push:
branches:
- master
- hotfix/*
- release/*
branches: [master]
pull_request:
branches:
- master
branches: [master]
jobs:
compile:
runs-on: ubuntu-latest
@@ -56,7 +52,6 @@ jobs:
- run: npm run test
build:
runs-on: ubuntu-latest
needs: [lint, format, compile, unittest]
name: "Build"
steps:
- uses: actions/checkout@v2
@@ -80,7 +75,6 @@ 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
@@ -107,7 +101,6 @@ 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
@@ -134,14 +127,8 @@ 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
@@ -168,14 +155,8 @@ 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
@@ -198,7 +179,7 @@ jobs:
NODE_TLS_REJECT_UNAUTHORIZED: 0
nuget:
name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
if: github.ref == 'refs/heads/master'
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest
env:
@@ -222,7 +203,7 @@ jobs:
path: "*.nupkg"
nugetmpac:
name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
if: github.ref == 'refs/heads/master'
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
runs-on: ubuntu-latest
env:

View File

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

View File

@@ -6,8 +6,8 @@
"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 --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 edge --headless",
"test:sql": "cypress run --browser chrome --headless --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"
},
"devDependencies": {

View File

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

94
package-lock.json generated
View File

@@ -5,14 +5,13 @@
"requires": true,
"dependencies": {
"@azure/cosmos": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.9.0.tgz",
"integrity": "sha512-SA+QB54I8Dvg/ZolHpsEDLK/sbSB9sFmSU1ElnMTFw88TVik+LYHq4o/srU2TY6Gr1BketjPmgLVEqrmnRvjkw==",
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.4.tgz",
"integrity": "sha512-IbSEadapQDajSCXj7gUc8OklkOd/oAY4w7XBLHouWc4iKQTtntb2DmGjhrbh2W5Ku+pmBSr1GTApCjQ55iIjlQ==",
"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",
@@ -23,14 +22,14 @@
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
},
"uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
}
}
},
@@ -7721,11 +7720,6 @@
"@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",
@@ -9572,11 +9566,6 @@
"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",
@@ -10133,9 +10122,9 @@
"dev": true
},
"canvas": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
"integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.0.tgz",
"integrity": "sha512-bEO9f1ThmbknLPxCa8Es7obPlN9W3stB1bo7njlhOFKIdUTldeTqXCh9YclCPAi2pSQs84XA0jq/QEZXSzgyMw==",
"requires": {
"nan": "^2.14.0",
"node-pre-gyp": "^0.11.0",
@@ -10940,14 +10929,6 @@
"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",
@@ -12818,12 +12799,6 @@
"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",
@@ -15637,14 +15612,6 @@
}
}
},
"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",
@@ -20205,11 +20172,6 @@
"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",
@@ -21540,9 +21502,9 @@
}
},
"needle": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz",
"integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
@@ -22297,11 +22259,11 @@
"integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo="
},
"p-retry": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz",
"integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==",
"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": {
"@types/retry": "^0.12.0",
"retry": "^0.12.0"
}
},
@@ -24079,7 +24041,8 @@
"retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
"dev": true
},
"reusify": {
"version": "1.0.4",
@@ -24636,9 +24599,9 @@
"integrity": "sha1-ZfDBWZNSs1Ny7KrlolDmEHN27Wk="
},
"simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
},
"simple-get": {
"version": "3.1.0",
@@ -27482,15 +27445,6 @@
"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",

View File

@@ -4,7 +4,7 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/cosmos": "3.9.0",
"@azure/cosmos": "3.7.4",
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1",
@@ -42,7 +42,7 @@
"applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0",
"bootstrap": "3.4.1",
"canvas": "2.6.1",
"canvas": "2.6.0",
"clean-webpack-plugin": "0.1.19",
"copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2",
@@ -56,7 +56,6 @@
"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",
@@ -67,7 +66,6 @@
"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",
@@ -135,7 +133,6 @@
"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",

View File

@@ -3,8 +3,8 @@
"offerThroughput": 400,
"databaseLevelThroughput": false,
"collectionId": "Persons",
"createNewDatabase": true,
"partitionKey": { "kind": "Hash", "paths": ["/firstname"], "version": 1 },
"rupmEnabled": false,
"partitionKey": { "kind": "Hash", "paths": ["/firstname"] },
"data": [
{
"firstname": "Eva",

13
src/Api/Apis.ts Normal file
View File

@@ -0,0 +1,13 @@
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 { configContext } from "../ConfigContext";
import { config } from "../Config";
import { HashMap } from "./HashMap";
export class AuthorizationEndpoints {
@@ -7,23 +7,14 @@ 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 = configContext.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
public static productionPortal: string = config.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
}
export class EndpointsRegex {
public static readonly cassandra = [
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
"HostName=(.*).cassandra.cosmos.azure.com"
];
public static readonly cassandra = "AccountEndpoint=(.*).cassandra.cosmosdb.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";
@@ -110,7 +101,6 @@ 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 {
@@ -122,8 +112,6 @@ 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";
@@ -134,7 +122,6 @@ 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 {

View File

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

View File

@@ -1,28 +1,39 @@
import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { configContext, Platform } from "../ConfigContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { userContext } from "../UserContext";
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;
const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo;
if (configContext.platform === Platform.Emulator) {
if (config.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 (userContext.masterKey) {
if (_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 (userContext.resourceToken) {
return userContext.resourceToken;
if (_resourceToken) {
return _resourceToken;
}
const result = await getTokenFromAuthService(verb, resourceType, resourceId);
@@ -31,33 +42,28 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
};
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
requestContext.endpoint = configContext.PROXY_PATH;
requestContext.endpoint = config.PROXY_PATH;
requestContext.headers["x-ms-proxy-target"] = endpoint();
return next(requestContext);
};
export const endpoint = () => {
if (configContext.platform === Platform.Emulator) {
if (config.platform === Platform.Emulator) {
// In worker scope, _global(self).parent does not exist
const location = _global.parent ? _global.parent.location : _global.location;
return configContext.EMULATOR_ENDPOINT || location.origin;
return config.EMULATOR_ENDPOINT || location.origin;
}
return (
userContext.endpoint ||
(userContext.databaseAccount &&
userContext.databaseAccount.properties &&
userContext.databaseAccount.properties.documentEndpoint)
);
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
};
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
try {
const host = configContext.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint();
const host = config.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": userContext.accessToken
"x-ms-encrypted-auth-token": _accessToken
},
body: JSON.stringify({
verb,
@@ -69,15 +75,22 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
const result = JSON.parse(await response.json());
return result;
} catch (error) {
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`
);
return Promise.reject(error);
}
}
export function client(): Cosmos.CosmosClient {
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: userContext.masterKey,
key: _masterKey,
tokenProvider,
connectionPolicy: {
enableEndpointDiscovery: false
@@ -89,5 +102,79 @@ export function client(): Cosmos.CosmosClient {
if (process.env.NODE_ENV === "development") {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
}
return new Cosmos.CosmosClient(options);
_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;
}
};

View File

@@ -6,23 +6,25 @@ import * as ViewModels from "../Contracts/ViewModels";
import Q from "q";
import {
ConflictDefinition,
ContainerDefinition,
ContainerResponse,
DatabaseResponse,
FeedOptions,
ItemDefinition,
PartitionKeyDefinition,
QueryIterator,
Resource,
TriggerDefinition,
OfferDefinition
TriggerDefinition
} from "@azure/cosmos";
import { client } from "./CosmosClient";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
import { CosmosClient } from "./CosmosClient";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { sendCachedDataMessage } from "./MessageHandler";
import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { OfferUtils } from "../Utils/OfferUtils";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { Platform, configContext } from "../ConfigContext";
import DocumentId from "../Explorer/Tree/DocumentId";
import ConflictId from "../Explorer/Tree/ConflictId";
export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
@@ -41,26 +43,28 @@ export function getCommonQueryOptions(options: FeedOptions): any {
return options;
}
export function queryDocuments(
// TODO: Add timeout for all promises
export abstract class DataAccessUtilityBase {
public queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
options = getCommonQueryOptions(options);
const documentsIterator = client()
const documentsIterator = CosmosClient.client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
return Q(documentsIterator);
}
export function readStoredProcedures(
public readStoredProcedures(
collection: ViewModels.Collection,
options?: any
): Q.Promise<DataModels.StoredProcedure[]> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedures.readAll(options)
@@ -69,13 +73,13 @@ export function readStoredProcedures(
);
}
export function readStoredProcedure(
public readStoredProcedure(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(requestedResource.id)
@@ -83,12 +87,12 @@ export function readStoredProcedure(
.then(response => response.resource as DataModels.StoredProcedure)
);
}
export function readUserDefinedFunctions(
public readUserDefinedFunctions(
collection: ViewModels.Collection,
options: any
): Q.Promise<DataModels.UserDefinedFunction[]> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunctions.readAll(options)
@@ -96,13 +100,13 @@ export function readUserDefinedFunctions(
.then(response => response.resources as DataModels.UserDefinedFunction[])
);
}
export function readUserDefinedFunction(
public readUserDefinedFunction(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.UserDefinedFunction> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunction(requestedResource.id)
@@ -111,9 +115,9 @@ export function readUserDefinedFunction(
);
}
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
public readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.triggers.readAll(options)
@@ -122,13 +126,13 @@ export function readTriggers(collection: ViewModels.Collection, options: any): Q
);
}
export function readTrigger(
public readTrigger(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.Trigger> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.trigger(requestedResource.id)
@@ -137,7 +141,7 @@ export function readTrigger(
);
}
export function executeStoredProcedure(
public executeStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: any,
@@ -146,7 +150,7 @@ export function executeStoredProcedure(
// TODO remove this deferred. Kept it because of timeout code at bottom of function
const deferred = Q.defer<any>();
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())
@@ -165,11 +169,11 @@ export function executeStoredProcedure(
);
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
public readDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
@@ -178,14 +182,14 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
);
}
export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
public getPartitionKeyHeaderForConflict(conflictId: ViewModels.ConflictId): Object {
const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey;
const partitionKeyValue: any = conflictId.partitionKeyValue;
return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
return this.getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
}
export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
public getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
if (!partitionKeyDefinition) {
return undefined;
}
@@ -197,15 +201,32 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
return [partitionKeyValue];
}
export function updateDocument(
public updateCollection(
databaseId: string,
collectionId: string,
newCollection: DataModels.Collection,
options: any = {}
): Q.Promise<DataModels.Collection> {
return Q(
CosmosClient.client()
.database(databaseId)
.container(collectionId)
.replace(newCollection as ContainerDefinition, options)
.then(async (response: ContainerResponse) => {
return this.refreshCachedResources().then(() => response.resource as DataModels.Collection);
})
);
}
public updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
documentId: ViewModels.DocumentId,
newDocument: any
): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
@@ -214,29 +235,28 @@ export function updateDocument(
);
}
export function updateOffer(
public updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options?: RequestOptions
): Q.Promise<DataModels.Offer> {
return Q(
client()
CosmosClient.client()
.offer(offer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.replace((newOffer as unknown) as OfferDefinition, options)
.replace(newOffer, options)
.then(response => {
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
return Promise.all([this.refreshCachedOffers(), this.refreshCachedResources()]).then(() => response.resource);
})
);
}
export function updateStoredProcedure(
public updateStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options: any
): Q.Promise<DataModels.StoredProcedure> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id)
@@ -245,13 +265,13 @@ export function updateStoredProcedure(
);
}
export function updateUserDefinedFunction(
public updateUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options?: any
): Q.Promise<DataModels.UserDefinedFunction> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunction(userDefinedFunction.id)
@@ -260,13 +280,13 @@ export function updateUserDefinedFunction(
);
}
export function updateTrigger(
public updateTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger,
options?: any
): Q.Promise<DataModels.Trigger> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.trigger(trigger.id)
@@ -275,9 +295,9 @@ export function updateTrigger(
);
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
public createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument)
@@ -285,13 +305,13 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
);
}
export function createStoredProcedure(
public createStoredProcedure(
collection: ViewModels.Collection,
newStoredProcedure: DataModels.StoredProcedure,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedures.create(newStoredProcedure, options)
@@ -299,13 +319,13 @@ export function createStoredProcedure(
);
}
export function createUserDefinedFunction(
public createUserDefinedFunction(
collection: ViewModels.Collection,
newUserDefinedFunction: DataModels.UserDefinedFunction,
options: any
): Q.Promise<DataModels.UserDefinedFunction> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunctions.create(newUserDefinedFunction, options)
@@ -313,13 +333,13 @@ export function createUserDefinedFunction(
);
}
export function createTrigger(
public createTrigger(
collection: ViewModels.Collection,
newTrigger: DataModels.Trigger,
options?: any
): Q.Promise<DataModels.Trigger> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.triggers.create(newTrigger as TriggerDefinition, options)
@@ -327,11 +347,11 @@ export function createTrigger(
);
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
public deleteDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
@@ -339,15 +359,15 @@ export function deleteDocument(collection: ViewModels.CollectionBase, documentId
);
}
export function deleteConflict(
public deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
conflictId: ViewModels.ConflictId,
options: any = {}
): Q.Promise<any> {
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
options.partitionKey = options.partitionKey || this.getPartitionKeyHeaderForConflict(conflictId);
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
@@ -355,13 +375,32 @@ export function deleteConflict(
);
}
export function deleteStoredProcedure(
public deleteCollection(collection: ViewModels.Collection, options: any): Q.Promise<any> {
return Q(
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.delete()
.then(() => this.refreshCachedResources())
);
}
public deleteDatabase(database: ViewModels.Database, options: any): Q.Promise<any> {
return Q(
CosmosClient.client()
.database(database.id())
.delete()
.then(() => this.refreshCachedResources())
);
}
public deleteStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options: any
): Q.Promise<any> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id)
@@ -369,13 +408,13 @@ export function deleteStoredProcedure(
);
}
export function deleteUserDefinedFunction(
public deleteUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options: any
): Q.Promise<any> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunction(userDefinedFunction.id)
@@ -383,13 +422,9 @@ export function deleteUserDefinedFunction(
);
}
export function deleteTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger,
options: any
): Q.Promise<any> {
public deleteTrigger(collection: ViewModels.Collection, trigger: DataModels.Trigger, options: any): Q.Promise<any> {
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.scripts.trigger(trigger.id)
@@ -397,7 +432,27 @@ export function deleteTrigger(
);
}
export function readCollectionQuotaInfo(
public readCollections(database: ViewModels.Database, options: any): Q.Promise<DataModels.Collection[]> {
return Q(
CosmosClient.client()
.database(database.id())
.containers.readAll()
.fetchAll()
.then(response => response.resources as DataModels.Collection[])
);
}
public readCollection(databaseId: string, collectionId: string): Q.Promise<DataModels.Collection> {
return Q(
CosmosClient.client()
.database(databaseId)
.container(collectionId)
.read()
.then(response => response.resource as DataModels.Collection)
);
}
public readCollectionQuotaInfo(
collection: ViewModels.Collection,
options: any
): Q.Promise<DataModels.CollectionQuotaInfo> {
@@ -407,7 +462,7 @@ export function readCollectionQuotaInfo(
options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true;
return Q(
client()
CosmosClient.client()
.database(collection.databaseId)
.container(collection.id())
.read(options)
@@ -432,37 +487,16 @@ export function readCollectionQuotaInfo(
);
}
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
if (options.isServerless) {
return Q([]); // Reading offers is not supported for serverless accounts
}
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
(<any>window).dataExplorer.databaseAccount().id,
Constants.ClientDefaults.portalCacheTimeoutMs
]);
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK
}
public readOffers(options: any): Q.Promise<DataModels.Offer[]> {
return Q(
client()
CosmosClient.client()
.offers.readAll()
.fetchAll()
.then(response => response.resources)
.catch(error => {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
throw error;
})
);
}
export function readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise<DataModels.OfferWithHeaders> {
public readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise<DataModels.OfferWithHeaders> {
options = options || {};
options.initialHeaders = options.initialHeaders || {};
if (!OfferUtils.isOfferV1(requestedResource)) {
@@ -470,38 +504,168 @@ export function readOffer(requestedResource: DataModels.Offer, options: any): Q.
}
return Q(
client()
CosmosClient.client()
.offer(requestedResource.id)
.read(options)
.then(response => ({ ...response.resource, headers: response.headers }))
);
}
export function refreshCachedOffers(): Q.Promise<void> {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
public readDatabases(options: any): Q.Promise<DataModels.Database[]> {
return Q(
CosmosClient.client()
.databases.readAll()
.fetchAll()
.then(response => response.resources as DataModels.Database[])
);
}
public getOrCreateDatabaseAndCollection(
request: DataModels.CreateDatabaseAndCollectionRequest,
options: any
): Q.Promise<DataModels.Collection> {
const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput");
const {
databaseId,
databaseLevelThroughput,
collectionId,
partitionKey,
indexingPolicy,
uniqueKeyPolicy,
offerThroughput,
analyticalStorageTtl,
hasAutoPilotV2FeatureFlag
} = request;
const createBody: DatabaseRequest = {
id: databaseId
};
// TODO: replace when SDK support autopilot
const initialHeaders = request.autoPilot
? !hasAutoPilotV2FeatureFlag
? {
[Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({
maxThroughput: request.autoPilot.maxThroughput
})
}
: {
[Constants.HttpHeaders.autoPilotTier]: request.autoPilot.autopilotTier
}
: undefined;
if (databaseLevelThroughput) {
if (request.autoPilot) {
databaseOptions.initialHeaders = initialHeaders;
}
createBody.throughput = offerThroughput;
}
return Q(
CosmosClient.client()
.databases.createIfNotExists(createBody, databaseOptions)
.then(response => {
return response.database.containers.create(
{
id: collectionId,
partitionKey: (partitionKey || undefined) as PartitionKeyDefinition,
indexingPolicy: indexingPolicy ? indexingPolicy : undefined,
uniqueKeyPolicy: uniqueKeyPolicy ? uniqueKeyPolicy : undefined,
analyticalStorageTtl: analyticalStorageTtl,
throughput: databaseLevelThroughput || request.autoPilot ? undefined : offerThroughput
} as ContainerRequest, // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
{
initialHeaders: databaseLevelThroughput ? undefined : initialHeaders
}
);
})
.then(containerResponse => containerResponse.resource as DataModels.Collection)
.finally(() => this.refreshCachedResources(options))
);
}
public createDatabase(request: DataModels.CreateDatabaseRequest, options: any): Q.Promise<DataModels.Database> {
var deferred = Q.defer<DataModels.Database>();
this._createDatabase(request, options).then(
(createdDatabase: DataModels.Database) => {
this.refreshCachedOffers().then(() => {
deferred.resolve(createdDatabase);
});
},
_createDatabaseError => {
deferred.reject(_createDatabaseError);
}
);
return deferred.promise;
}
public refreshCachedOffers(): Q.Promise<void> {
if (MessageHandler.canSendMessage()) {
return MessageHandler.sendCachedDataMessage(MessageTypes.RefreshOffers, []);
} else {
return Q();
}
}
export function refreshCachedResources(options?: any): Q.Promise<void> {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshResources, []);
public refreshCachedResources(options?: any): Q.Promise<void> {
if (MessageHandler.canSendMessage()) {
return MessageHandler.sendCachedDataMessage(MessageTypes.RefreshResources, []);
} else {
return Q();
}
}
export function queryConflicts(
public queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = client()
const documentsIterator = CosmosClient.client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
return Q(documentsIterator);
}
public updateOfferThroughputBeyondLimit(
request: DataModels.UpdateOfferThroughputRequest,
options: any
): Q.Promise<void> {
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
}
private _createDatabase(
request: DataModels.CreateDatabaseRequest,
options: any = {}
): Q.Promise<DataModels.Database> {
const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request;
const createBody: DatabaseRequest = { id: databaseId };
const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput");
// TODO: replace when SDK support autopilot
const initialHeaders = autoPilot
? !hasAutoPilotV2FeatureFlag
? {
[Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({ maxThroughput: autoPilot.maxThroughput })
}
: {
[Constants.HttpHeaders.autoPilotTier]: autoPilot.autopilotTier
}
: undefined;
if (!!databaseLevelThroughput) {
if (autoPilot) {
databaseOptions.initialHeaders = initialHeaders;
}
createBody.throughput = offerThroughput;
}
return Q(
CosmosClient.client()
.databases.create(createBody, databaseOptions)
.then((response: DatabaseResponse) => {
return this.refreshCachedResources(databaseOptions).then(() => response.resource);
})
);
}
}

View File

@@ -1,39 +1,42 @@
import * as Constants from "./Constants";
import * as DataModels from "../Contracts/DataModels";
import * as ErrorParserUtility from "./ErrorParserUtility";
import * as ViewModels from "../Contracts/ViewModels";
import Q from "q";
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import { DataAccessUtilityBase } from "./DataAccessUtilityBase";
import * as Logger from "./Logger";
import { MessageHandler } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import { sendNotificationForError } from "./dataAccess/sendNotificationForError";
// TODO: Log all promise resolutions and errors with verbosity levels
export function queryDocuments(
export default class DocumentClientUtilityBase {
constructor(private _dataAccessUtility: DataAccessUtilityBase) {}
public queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
return this._dataAccessUtility.queryDocuments(databaseId, containerId, query, options);
}
export function queryConflicts(
public queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
return this._dataAccessUtility.queryConflicts(databaseId, containerId, query, options);
}
export function getEntityName() {
public getEntityName() {
const defaultExperience =
window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience();
if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) {
@@ -42,7 +45,7 @@ export function getEntityName() {
return "item";
}
export function readStoredProcedures(
public readStoredProcedures(
collection: ViewModels.Collection,
options: any = {}
): Q.Promise<DataModels.StoredProcedure[]> {
@@ -51,7 +54,8 @@ export function readStoredProcedures(
ConsoleDataType.InProgress,
`Querying stored procedures for container ${collection.id()}`
);
DataAccessUtilityBase.readStoredProcedures(collection, options)
this._dataAccessUtility
.readStoredProcedures(collection, options)
.then(
(storedProcedures: DataModels.StoredProcedure[]) => {
deferred.resolve(storedProcedures);
@@ -62,7 +66,7 @@ export function readStoredProcedures(
`Failed to query stored procedures for container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -73,15 +77,15 @@ export function readStoredProcedures(
return deferred.promise;
}
export function readStoredProcedure(
public readStoredProcedure(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
return DataAccessUtilityBase.readStoredProcedure(collection, requestedResource, options);
return this._dataAccessUtility.readStoredProcedure(collection, requestedResource, options);
}
export function readUserDefinedFunctions(
public readUserDefinedFunctions(
collection: ViewModels.Collection,
options: any = {}
): Q.Promise<DataModels.UserDefinedFunction[]> {
@@ -90,7 +94,8 @@ export function readUserDefinedFunctions(
ConsoleDataType.InProgress,
`Querying user defined functions for collection ${collection.id()}`
);
DataAccessUtilityBase.readUserDefinedFunctions(collection, options)
this._dataAccessUtility
.readUserDefinedFunctions(collection, options)
.then(
(userDefinedFunctions: DataModels.UserDefinedFunction[]) => {
deferred.resolve(userDefinedFunctions);
@@ -101,7 +106,7 @@ export function readUserDefinedFunctions(
`Failed to query user defined functions for container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadUDFs", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -112,22 +117,23 @@ export function readUserDefinedFunctions(
return deferred.promise;
}
export function readUserDefinedFunction(
public readUserDefinedFunction(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options: any
): Q.Promise<DataModels.UserDefinedFunction> {
return DataAccessUtilityBase.readUserDefinedFunction(collection, requestedResource, options);
return this._dataAccessUtility.readUserDefinedFunction(collection, requestedResource, options);
}
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
public readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
var deferred = Q.defer<DataModels.Trigger[]>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying triggers for container ${collection.id()}`
);
DataAccessUtilityBase.readTriggers(collection, options)
this._dataAccessUtility
.readTriggers(collection, options)
.then(
(triggers: DataModels.Trigger[]) => {
deferred.resolve(triggers);
@@ -138,7 +144,7 @@ export function readTriggers(collection: ViewModels.Collection, options: any): Q
`Failed to query triggers for container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadTriggers", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -149,15 +155,15 @@ export function readTriggers(collection: ViewModels.Collection, options: any): Q
return deferred.promise;
}
export function readTrigger(
public readTrigger(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.Trigger> {
return DataAccessUtilityBase.readTrigger(collection, requestedResource, options);
return this._dataAccessUtility.readTrigger(collection, requestedResource, options);
}
export function executeStoredProcedure(
public executeStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: any,
@@ -169,7 +175,8 @@ export function executeStoredProcedure(
ConsoleDataType.InProgress,
`Executing stored procedure ${storedProcedure.id()}`
);
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
this._dataAccessUtility
.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then(
(response: any) => {
deferred.resolve(response);
@@ -186,7 +193,7 @@ export function executeStoredProcedure(
)}`
);
Logger.logError(JSON.stringify(error), "ExecuteStoredProcedure", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -197,14 +204,14 @@ export function executeStoredProcedure(
return deferred.promise;
}
export function queryDocumentsPage(
public queryDocumentsPage(
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
const entityName = getEntityName();
const entityName = this.getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying ${entityName} for container ${resourceName}`
@@ -225,7 +232,7 @@ export function queryDocumentsPage(
`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -236,14 +243,15 @@ export function queryDocumentsPage(
return deferred.promise;
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
public readDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const entityName = this.getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Reading ${entityName} ${documentId.id()}`
);
DataAccessUtilityBase.readDocument(collection, documentId)
this._dataAccessUtility
.readDocument(collection, documentId)
.then(
(document: any) => {
deferred.resolve(document);
@@ -254,7 +262,7 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadDocument", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -265,18 +273,56 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
return deferred.promise;
}
export function updateDocument(
public updateCollection(
databaseId: string,
collection: ViewModels.Collection,
newCollection: DataModels.Collection
): Q.Promise<DataModels.Collection> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating container ${collection.id()}`
);
this._dataAccessUtility
.updateCollection(databaseId, collection.id(), newCollection)
.then(
(replacedCollection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated container ${collection.id()}`
);
deferred.resolve(replacedCollection);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to update container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateCollection", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
documentId: ViewModels.DocumentId,
newDocument: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const entityName = this.getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating ${entityName} ${documentId.id()}`
);
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
this._dataAccessUtility
.updateDocument(collection, documentId, newDocument)
.then(
(updatedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -291,7 +337,7 @@ export function updateDocument(
`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateDocument", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -302,7 +348,7 @@ export function updateDocument(
return deferred.promise;
}
export function updateOffer(
public updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options: RequestOptions
@@ -312,7 +358,8 @@ export function updateOffer(
ConsoleDataType.InProgress,
`Updating offer for resource ${offer.resource}`
);
DataAccessUtilityBase.updateOffer(offer, newOffer, options)
this._dataAccessUtility
.updateOffer(offer, newOffer, options)
.then(
(replacedOffer: DataModels.Offer) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -335,7 +382,7 @@ export function updateOffer(
"UpdateOffer",
error.code
);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -346,7 +393,43 @@ export function updateOffer(
return deferred.promise;
}
export function updateStoredProcedure(
public updateOfferThroughputBeyondLimit(
requestPayload: DataModels.UpdateOfferThroughputRequest,
options: any = {}
): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>();
const resourceDescriptionInfo: string = requestPayload.collectionName
? `database ${requestPayload.databaseName} and container ${requestPayload.collectionName}`
: `database ${requestPayload.databaseName}`;
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Requesting increase in throughput to ${requestPayload.throughput} for ${resourceDescriptionInfo}`
);
this._dataAccessUtility
.updateOfferThroughputBeyondLimit(requestPayload, options)
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully requested an increase in throughput to ${requestPayload.throughput} for ${resourceDescriptionInfo}`
);
deferred.resolve();
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to request an increase in throughput for ${requestPayload.throughput}: ${JSON.stringify(error)}`
);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
}
public updateStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options?: any
@@ -356,7 +439,8 @@ export function updateStoredProcedure(
ConsoleDataType.InProgress,
`Updating stored procedure ${storedProcedure.id}`
);
DataAccessUtilityBase.updateStoredProcedure(collection, storedProcedure, options)
this._dataAccessUtility
.updateStoredProcedure(collection, storedProcedure, options)
.then(
(updatedStoredProcedure: DataModels.StoredProcedure) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -371,7 +455,7 @@ export function updateStoredProcedure(
`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -382,7 +466,7 @@ export function updateStoredProcedure(
return deferred.promise;
}
export function updateUserDefinedFunction(
public updateUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options: any = {}
@@ -392,7 +476,8 @@ export function updateUserDefinedFunction(
ConsoleDataType.InProgress,
`Updating user defined function ${userDefinedFunction.id}`
);
DataAccessUtilityBase.updateUserDefinedFunction(collection, userDefinedFunction, options)
this._dataAccessUtility
.updateUserDefinedFunction(collection, userDefinedFunction, options)
.then(
(updatedUserDefinedFunction: DataModels.UserDefinedFunction) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -407,7 +492,7 @@ export function updateUserDefinedFunction(
`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateUDF", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -418,13 +503,11 @@ export function updateUserDefinedFunction(
return deferred.promise;
}
export function updateTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger
): Q.Promise<DataModels.Trigger> {
public updateTrigger(collection: ViewModels.Collection, trigger: DataModels.Trigger): Q.Promise<DataModels.Trigger> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Updating trigger ${trigger.id}`);
DataAccessUtilityBase.updateTrigger(collection, trigger)
this._dataAccessUtility
.updateTrigger(collection, trigger)
.then(
(updatedTrigger: DataModels.Trigger) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Updated trigger ${trigger.id}`);
@@ -436,7 +519,7 @@ export function updateTrigger(
`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateTrigger", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -447,14 +530,15 @@ export function updateTrigger(
return deferred.promise;
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
public createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const entityName = this.getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating new ${entityName} for container ${collection.id()}`
);
DataAccessUtilityBase.createDocument(collection, newDocument)
this._dataAccessUtility
.createDocument(collection, newDocument)
.then(
(savedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -469,7 +553,7 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
`Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateDocument", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -480,7 +564,7 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
return deferred.promise;
}
export function createStoredProcedure(
public createStoredProcedure(
collection: ViewModels.Collection,
newStoredProcedure: DataModels.StoredProcedure,
options?: any
@@ -490,7 +574,8 @@ export function createStoredProcedure(
ConsoleDataType.InProgress,
`Creating stored procedure for container ${collection.id()}`
);
DataAccessUtilityBase.createStoredProcedure(collection, newStoredProcedure, options)
this._dataAccessUtility
.createStoredProcedure(collection, newStoredProcedure, options)
.then(
(createdStoredProcedure: DataModels.StoredProcedure) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -505,7 +590,7 @@ export function createStoredProcedure(
`Error while creating stored procedure for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -516,7 +601,7 @@ export function createStoredProcedure(
return deferred.promise;
}
export function createUserDefinedFunction(
public createUserDefinedFunction(
collection: ViewModels.Collection,
newUserDefinedFunction: DataModels.UserDefinedFunction,
options?: any
@@ -526,7 +611,8 @@ export function createUserDefinedFunction(
ConsoleDataType.InProgress,
`Creating user defined function for container ${collection.id()}`
);
DataAccessUtilityBase.createUserDefinedFunction(collection, newUserDefinedFunction, options)
this._dataAccessUtility
.createUserDefinedFunction(collection, newUserDefinedFunction, options)
.then(
(createdUserDefinedFunction: DataModels.UserDefinedFunction) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -541,7 +627,7 @@ export function createUserDefinedFunction(
`Error while creating user defined function for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateUDF", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -552,7 +638,7 @@ export function createUserDefinedFunction(
return deferred.promise;
}
export function createTrigger(
public createTrigger(
collection: ViewModels.Collection,
newTrigger: DataModels.Trigger,
options: any = {}
@@ -562,7 +648,8 @@ export function createTrigger(
ConsoleDataType.InProgress,
`Creating trigger for container ${collection.id()}`
);
DataAccessUtilityBase.createTrigger(collection, newTrigger, options)
this._dataAccessUtility
.createTrigger(collection, newTrigger, options)
.then(
(createdTrigger: DataModels.Trigger) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -577,7 +664,7 @@ export function createTrigger(
`Error while creating trigger for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateTrigger", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -588,14 +675,15 @@ export function createTrigger(
return deferred.promise;
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
public deleteDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const entityName = this.getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting ${entityName} ${documentId.id()}`
);
DataAccessUtilityBase.deleteDocument(collection, documentId)
this._dataAccessUtility
.deleteDocument(collection, documentId)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -610,7 +698,7 @@ export function deleteDocument(collection: ViewModels.CollectionBase, documentId
`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteDocument", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -621,9 +709,9 @@ export function deleteDocument(collection: ViewModels.CollectionBase, documentId
return deferred.promise;
}
export function deleteConflict(
public deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
conflictId: ViewModels.ConflictId,
options?: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
@@ -632,7 +720,8 @@ export function deleteConflict(
ConsoleDataType.InProgress,
`Deleting conflict ${conflictId.id()}`
);
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
this._dataAccessUtility
.deleteConflict(collection, conflictId, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -647,7 +736,7 @@ export function deleteConflict(
`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteConflict", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -658,7 +747,75 @@ export function deleteConflict(
return deferred.promise;
}
export function deleteStoredProcedure(
public deleteCollection(collection: ViewModels.Collection, options: any = {}): Q.Promise<any> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting container ${collection.id()}`
);
this._dataAccessUtility
.deleteCollection(collection, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted container ${collection.id()}`
);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteCollection", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public deleteDatabase(database: ViewModels.Database, options: any = {}): Q.Promise<any> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting database ${database.id()}`
);
this._dataAccessUtility
.deleteDatabase(database, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted database ${database.id()}`
);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting database ${database.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteDatabase", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public deleteStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options: any = {}
@@ -669,7 +826,8 @@ export function deleteStoredProcedure(
ConsoleDataType.InProgress,
`Deleting stored procedure ${storedProcedure.id}`
);
DataAccessUtilityBase.deleteStoredProcedure(collection, storedProcedure, options)
this._dataAccessUtility
.deleteStoredProcedure(collection, storedProcedure, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -684,7 +842,7 @@ export function deleteStoredProcedure(
`Error while deleting stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -695,7 +853,7 @@ export function deleteStoredProcedure(
return deferred.promise;
}
export function deleteUserDefinedFunction(
public deleteUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options: any = {}
@@ -705,7 +863,8 @@ export function deleteUserDefinedFunction(
ConsoleDataType.InProgress,
`Deleting user defined function ${userDefinedFunction.id}`
);
DataAccessUtilityBase.deleteUserDefinedFunction(collection, userDefinedFunction, options)
this._dataAccessUtility
.deleteUserDefinedFunction(collection, userDefinedFunction, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -720,7 +879,7 @@ export function deleteUserDefinedFunction(
`Error while deleting user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteUDF", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -731,17 +890,21 @@ export function deleteUserDefinedFunction(
return deferred.promise;
}
export function deleteTrigger(
public deleteTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger,
options: any = {}
): Q.Promise<DataModels.Trigger> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Deleting trigger ${trigger.id}`);
DataAccessUtilityBase.deleteTrigger(collection, trigger, options)
this._dataAccessUtility
.deleteTrigger(collection, trigger, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted trigger ${trigger.id}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted trigger ${trigger.id}`
);
deferred.resolve(response);
},
(error: any) => {
@@ -750,7 +913,7 @@ export function deleteTrigger(
`Error while deleting trigger ${trigger.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteTrigger", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -761,15 +924,74 @@ export function deleteTrigger(
return deferred.promise;
}
export function refreshCachedResources(options: any = {}): Q.Promise<void> {
return DataAccessUtilityBase.refreshCachedResources(options);
public refreshCachedResources(options: any = {}): Q.Promise<void> {
return this._dataAccessUtility.refreshCachedResources(options);
}
export function refreshCachedOffers(): Q.Promise<void> {
return DataAccessUtilityBase.refreshCachedOffers();
public refreshCachedOffers(): Q.Promise<void> {
return this._dataAccessUtility.refreshCachedOffers();
}
export function readCollectionQuotaInfo(
public readCollections(database: ViewModels.Database, options: any = {}): Q.Promise<DataModels.Collection[]> {
var deferred = Q.defer<DataModels.Collection[]>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying containers for database ${database.id()}`
);
this._dataAccessUtility
.readCollections(database, options)
.then(
(collections: DataModels.Collection[]) => {
deferred.resolve(collections);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying containers for database ${database.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadCollections", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public readCollection(databaseId: string, collectionId: string): Q.Promise<DataModels.Collection> {
const deferred = Q.defer<DataModels.Collection>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying container ${collectionId}`
);
this._dataAccessUtility
.readCollection(databaseId, collectionId)
.then(
(collection: DataModels.Collection) => {
deferred.resolve(collection);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadCollections", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public readCollectionQuotaInfo(
collection: ViewModels.Collection,
options?: any
): Q.Promise<DataModels.CollectionQuotaInfo> {
@@ -779,7 +1001,8 @@ export function readCollectionQuotaInfo(
ConsoleDataType.InProgress,
`Querying quota info for container ${collection.id}`
);
DataAccessUtilityBase.readCollectionQuotaInfo(collection, options)
this._dataAccessUtility
.readCollectionQuotaInfo(collection, options)
.then(
(quota: DataModels.CollectionQuotaInfo) => {
deferred.resolve(quota);
@@ -790,7 +1013,7 @@ export function readCollectionQuotaInfo(
`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -801,11 +1024,12 @@ export function readCollectionQuotaInfo(
return deferred.promise;
}
export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
public readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
var deferred = Q.defer<DataModels.Offer[]>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offers");
DataAccessUtilityBase.readOffers(options)
this._dataAccessUtility
.readOffers(options)
.then(
(offers: DataModels.Offer[]) => {
deferred.resolve(offers);
@@ -816,7 +1040,7 @@ export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
`Error while querying offers:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadOffers", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -827,14 +1051,12 @@ export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
return deferred.promise;
}
export function readOffer(
requestedResource: DataModels.Offer,
options: any = {}
): Q.Promise<DataModels.OfferWithHeaders> {
public readOffer(requestedResource: DataModels.Offer, options: any = {}): Q.Promise<DataModels.OfferWithHeaders> {
var deferred = Q.defer<DataModels.OfferWithHeaders>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offer");
DataAccessUtilityBase.readOffer(requestedResource, options)
this._dataAccessUtility
.readOffer(requestedResource, options)
.then(
(offer: DataModels.OfferWithHeaders) => {
deferred.resolve(offer);
@@ -845,7 +1067,7 @@ export function readOffer(
`Error while querying offer:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadOffer", error.code);
sendNotificationForError(error);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
@@ -855,3 +1077,108 @@ export function readOffer(
return deferred.promise;
}
public readDatabases(options: any): Q.Promise<DataModels.Database[]> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying databases");
this._dataAccessUtility
.readDatabases(options)
.then(
(databases: DataModels.Database[]) => {
deferred.resolve(databases);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying databases:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadDatabases", error.code);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
public getOrCreateDatabaseAndCollection(
request: DataModels.CreateDatabaseAndCollectionRequest,
options: any = {}
): Q.Promise<DataModels.Collection> {
const deferred: Q.Deferred<DataModels.Collection> = Q.defer<DataModels.Collection>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating a new container ${request.collectionId} for database ${request.databaseId}`
);
this._dataAccessUtility
.getOrCreateDatabaseAndCollection(request, options)
.then(
(collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created container ${request.collectionId}`
);
deferred.resolve(collection);
},
(error: any) => {
const sanitizedError = ErrorParserUtility.replaceKnownError(JSON.stringify(error));
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while creating container ${request.collectionId}:\n ${sanitizedError}`
);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
return deferred.promise;
}
public createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise<DataModels.Database> {
const deferred: Q.Deferred<DataModels.Database> = Q.defer<DataModels.Database>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating a new database ${request.databaseId}`
);
this._dataAccessUtility
.createDatabase(request, options)
.then(
(database: DataModels.Database) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created database ${request.databaseId}`
);
deferred.resolve(database);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while creating database ${request.databaseId}:\n ${JSON.stringify(error)}`
);
this.sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
return deferred.promise;
}
public sendNotificationForError(error: any) {
if (error && error.code === Constants.HttpStatusCodes.Forbidden) {
if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
MessageHandler.sendMessage({
type: MessageTypes.ForbiddenError,
reason: error && error.message ? error.message : error
});
}
}
}

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,8 +7,6 @@ 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,26 +1,46 @@
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(() => {
jest.resetAllMocks();
sendMessageSpy = spyOn(MessageHandler, "sendMessage");
});
afterEach(() => {
sendMessageSpy = null;
});
it("should log info messages", () => {
Logger.logInfo("Test info", "DocDB");
expect(sendMessage).toBeCalled();
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");
});
it("should log error messages", () => {
Logger.logError("Test error", "DocDB");
expect(sendMessage).toBeCalled();
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");
});
it("should log warnings", () => {
Logger.logWarning("Test warning", "DocDB");
expect(sendMessage).toBeCalled();
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");
});
});

View File

@@ -1,4 +1,4 @@
import { sendMessage } from "./MessageHandler";
import { MessageHandler } 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 {
sendMessage({
MessageHandler.sendMessage({
type: MessageTypes.LogInfo,
data: JSON.stringify(entry)
});

View File

@@ -1,29 +1,65 @@
import Q from "q";
import * as MessageHandler from "./MessageHandler";
import { CachedDataPromise, 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", () => {
it("should handle cached message", async () => {
let mockPromise = {
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> = {
id: "123",
startTime: new Date(),
deferred: Q.defer<any>()
};
let mockMessage = { message: { id: "123", data: "{}" } };
MessageHandler.RequestMap[mockPromise.id] = mockPromise;
MessageHandler.handleCachedDataMessage(mockMessage);
MockMessageHandler.addToMap(mockPromise.id, mockPromise);
MockMessageHandler.handleCachedDataMessage(mockMessage);
expect(mockPromise.deferred.promise.isFulfilled()).toBe(true);
});
it("should delete fulfilled promises on running the garbage collector", async () => {
let message = {
it("should delete fulfilled promises on running the garbage collector", () => {
let mockPromise: CachedDataPromise<any> = {
id: "123",
startTime: new Date(),
deferred: Q.defer<any>()
};
MessageHandler.handleCachedDataMessage(message);
MessageHandler.runGarbageCollector();
expect(MessageHandler.RequestMap["123"]).toBeUndefined();
MockMessageHandler.addToMap(mockPromise.id, mockPromise);
mockPromise.deferred.reject("some error");
MockMessageHandler.runGarbageCollector();
expect(MockMessageHandler.mapContainsKey(mockPromise.id)).toBe(false);
});
});

View File

@@ -9,24 +9,36 @@ export interface CachedDataPromise<T> {
id: string;
}
export const RequestMap: Record<string, CachedDataPromise<any>> = {};
/**
* 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 function handleCachedDataMessage(message: any): void {
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 || !RequestMap[messageContent.id]) {
if (
message == null ||
messageContent == null ||
messageContent.id == null ||
!MessageHandler.RequestMap[messageContent.id]
) {
return;
}
const cachedDataPromise = RequestMap[messageContent.id];
const cachedDataPromise = MessageHandler.RequestMap[messageContent.id];
if (messageContent.error != null) {
cachedDataPromise.deferred.reject(messageContent.error);
} else {
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
}
runGarbageCollector();
MessageHandler.runGarbageCollector();
}
export function sendCachedDataMessage<TResponseDataModel>(
public static sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes,
params: Object[],
timeoutInMs?: number
@@ -36,8 +48,8 @@ export function sendCachedDataMessage<TResponseDataModel>(
startTime: new Date(),
id: _.uniqueId()
};
RequestMap[cachedDataPromise.id] = cachedDataPromise;
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
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(
@@ -46,8 +58,8 @@ export function sendCachedDataMessage<TResponseDataModel>(
);
}
export function sendMessage(data: any): void {
if (canSendMessage()) {
public static sendMessage(data: any): void {
if (MessageHandler.canSendMessage()) {
window.parent.postMessage(
{
signature: "pcIframe",
@@ -58,16 +70,16 @@ export function sendMessage(data: any): void {
}
}
export function canSendMessage(): boolean {
public static 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;
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 RequestMap[key];
delete MessageHandler.RequestMap[key];
}
});
}
}

View File

@@ -1,18 +1,16 @@
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,
_createMongoCollectionWithARM
updateDocument
} 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";
@@ -62,15 +60,13 @@ const databaseAccount = {
tableEndpoint: "foo",
cassandraEndpoint: "foo"
}
} as DatabaseAccount;
};
describe("MongoProxyClient", () => {
describe("queryDocuments", () => {
beforeEach(() => {
resetConfigContext();
updateUserContext({
databaseAccount
});
delete config.BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -90,7 +86,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
config.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",
@@ -100,10 +96,8 @@ describe("MongoProxyClient", () => {
});
describe("readDocument", () => {
beforeEach(() => {
resetConfigContext();
updateUserContext({
databaseAccount
});
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -123,7 +117,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
config.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",
@@ -133,10 +127,8 @@ describe("MongoProxyClient", () => {
});
describe("createDocument", () => {
beforeEach(() => {
resetConfigContext();
updateUserContext({
databaseAccount
});
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -156,7 +148,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
config.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",
@@ -166,10 +158,8 @@ describe("MongoProxyClient", () => {
});
describe("updateDocument", () => {
beforeEach(() => {
resetConfigContext();
updateUserContext({
databaseAccount
});
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -181,7 +171,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)
@@ -189,8 +179,8 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
updateDocument(databaseId, collection, documentId, "{}");
config.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)
@@ -199,10 +189,8 @@ describe("MongoProxyClient", () => {
});
describe("deleteDocument", () => {
beforeEach(() => {
resetConfigContext();
updateUserContext({
databaseAccount
});
delete config.MONGO_BACKEND_ENDPOINT;
CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -222,7 +210,7 @@ describe("MongoProxyClient", () => {
});
it("builds the correct proxy URL in development", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
config.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",
@@ -232,11 +220,9 @@ describe("MongoProxyClient", () => {
});
describe("getEndpoint", () => {
beforeEach(() => {
resetConfigContext();
delete config.MONGO_BACKEND_ENDPOINT;
delete window.authType;
updateUserContext({
databaseAccount
});
CosmosClient.databaseAccount(databaseAccount as any);
window.dataExplorer = {
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
serverId: () => ""
@@ -249,7 +235,7 @@ describe("MongoProxyClient", () => {
});
it("returns a development endpoint", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
config.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 { MessageTypes } from "../Contracts/ExplorerContracts";
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 * 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 { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler";
const defaultHeaders = {
[HttpHeaders.apiType]: ApiType.MongoDB.toString(),
@@ -25,9 +26,9 @@ const defaultHeaders = {
function authHeaders() {
if (window.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
return { [HttpHeaders.guestAccessToken]: CosmosClient.accessToken() };
} else {
return { [HttpHeaders.authorization]: userContext.authorizationToken };
return { [HttpHeaders.authorization]: CosmosClient.authorizationToken() };
}
}
@@ -66,7 +67,7 @@ export function queryDocuments(
query: string,
continuationToken?: string
): Promise<QueryResponse> {
const databaseAccount = userContext.databaseAccount;
const databaseAccount = CosmosClient.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: userContext.subscriptionId,
rg: userContext.resourceGroup,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name,
pk:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
@@ -122,9 +123,9 @@ export function queryDocuments(
export function readDocument(
databaseId: string,
collection: Collection,
documentId: DocumentId
documentId: ViewModels.DocumentId
): Promise<DataModels.DocumentId> {
const databaseAccount = userContext.databaseAccount;
const databaseAccount = CosmosClient.databaseAccount();
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 4).join("/");
@@ -135,8 +136,8 @@ export function readDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid,
rtype: "docs",
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -168,7 +169,7 @@ export function createDocument(
partitionKeyProperty: string,
documentContent: unknown
): Promise<DataModels.DocumentId> {
const databaseAccount = userContext.databaseAccount;
const databaseAccount = CosmosClient.databaseAccount();
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = {
db: databaseId,
@@ -176,8 +177,8 @@ export function createDocument(
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
rid: collection.rid,
rtype: "docs",
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name,
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
};
@@ -204,10 +205,10 @@ export function createDocument(
export function updateDocument(
databaseId: string,
collection: Collection,
documentId: DocumentId,
documentContent: string
documentId: ViewModels.DocumentId,
documentContent: unknown
): Promise<DataModels.DocumentId> {
const databaseAccount = userContext.databaseAccount;
const databaseAccount = CosmosClient.databaseAccount();
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/");
@@ -218,8 +219,8 @@ export function updateDocument(
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid,
rtype: "docs",
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -229,7 +230,7 @@ export function updateDocument(
return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
method: "PUT",
body: documentContent,
body: JSON.stringify(documentContent),
headers: {
...defaultHeaders,
...authHeaders(),
@@ -245,8 +246,12 @@ export function updateDocument(
});
}
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
const databaseAccount = userContext.databaseAccount;
export function deleteDocument(
databaseId: string,
collection: Collection,
documentId: ViewModels.DocumentId
): Promise<void> {
const databaseAccount = CosmosClient.databaseAccount();
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/");
@@ -257,8 +262,8 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
rid,
rtype: "docs",
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
@@ -284,35 +289,43 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
}
export function createMongoCollectionWithProxy(
params: DataModels.CreateCollectionParams
databaseId: string,
collectionId: string,
offerThroughput: number,
shardKey: string,
createDatabase: boolean,
sharedThroughput: boolean,
isSharded: boolean,
autopilotOptions?: DataModels.RpOptions
): Promise<DataModels.Collection> {
const databaseAccount = userContext.databaseAccount;
const shardKey: string = params.partitionKey?.paths[0];
const mongoParams: DataModels.MongoParameters = {
const databaseAccount = CosmosClient.databaseAccount();
const params: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: params.databaseId,
coll: params.collectionId,
db: databaseId,
coll: collectionId,
pk: shardKey,
offerThroughput: params.offerThroughput,
cd: params.createNewDatabase,
st: params.databaseLevelThroughput,
is: !!shardKey,
offerThroughput,
cd: createDatabase,
st: sharedThroughput,
is: isSharded,
rid: "",
rtype: "colls",
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name,
isAutoPilot: !!params.autoPilotMaxThroughput,
autoPilotThroughput: params.autoPilotMaxThroughput?.toString()
isAutoPilot: false
};
if (autopilotOptions) {
params.isAutoPilot = true;
params.autoPilotTier = autopilotOptions[Constants.HttpHeaders.autoPilotTier] as string;
}
const endpoint = getEndpoint(databaseAccount);
return window
.fetch(
`${endpoint}/createCollection?${queryString.stringify(
(mongoParams as unknown) as queryString.ParsedUrlQueryInput
)}`,
`${endpoint}/createCollection?${queryString.stringify((params as unknown) as queryString.ParsedUrlQueryInput)}`,
{
method: "POST",
headers: {
@@ -326,7 +339,7 @@ export function createMongoCollectionWithProxy(
if (response.ok) {
return response.json();
}
return errorHandling(response, "creating collection", mongoParams);
return errorHandling(response, "creating collection", params);
});
}
@@ -342,7 +355,7 @@ export function createMongoCollectionWithARM(
isSharded: boolean,
additionalOptions?: DataModels.RpOptions
): Promise<DataModels.CreateCollectionWithRpResponse> {
const databaseAccount = userContext.databaseAccount;
const databaseAccount = CosmosClient.databaseAccount();
const params: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: databaseId,
@@ -354,8 +367,8 @@ export function createMongoCollectionWithARM(
is: isSharded,
rid: "",
rtype: "colls",
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
dba: databaseAccount.name,
analyticalStorageTtl
};
@@ -372,11 +385,11 @@ export function createMongoCollectionWithARM(
return _createMongoCollectionWithARM(armEndpoint, params, additionalOptions);
}
export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
export function getEndpoint(databaseAccount: ViewModels.DatabaseAccount): string {
const serverId = window.dataExplorer.serverId();
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
let url = configContext.MONGO_BACKEND_ENDPOINT
? configContext.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
let url = config.MONGO_BACKEND_ENDPOINT
? config.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
if (window.authType === AuthType.EncryptedToken) {
@@ -395,14 +408,16 @@ async function errorHandling(response: Response, action: string, params: unknown
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
);
if (response.status === HttpStatusCodes.Forbidden) {
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
MessageHandler.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/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${
CosmosClient.databaseAccount().name
}/mongodbDatabases/${params.db}/collections/${params.coll}`;
}
export async function _createMongoCollectionWithARM(

View File

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

View File

@@ -1,21 +1,18 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
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, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
import { createCollection } from "./dataAccess/createCollection";
import * as ErrorParserUtility from "./ErrorParserUtility";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
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 { QueryUtils } from "../Utils/QueryUtils";
import Explorer from "../Explorer/Explorer";
export class QueriesClient {
export class QueriesClient implements ViewModels.QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = {
paths: [`/${SavedQueries.PartitionKeyProperty}`],
kind: BackendDefaults.partitionKeyKind,
@@ -36,13 +33,13 @@ export class QueriesClient {
ConsoleDataType.InProgress,
"Setting up account for saving queries"
);
return createCollection({
return this.container.documentClientUtility
.getOrCreateDatabaseAndCollection({
collectionId: SavedQueries.CollectionName,
createNewDatabase: true,
databaseId: SavedQueries.DatabaseName,
partitionKey: QueriesClient.PartitionKey,
offerThroughput: SavedQueries.OfferThroughput,
databaseLevelThroughput: false
databaseLevelThroughput: undefined
})
.then(
(collection: DataModels.Collection) => {
@@ -92,7 +89,8 @@ export class QueriesClient {
`Saving query ${query.queryName}`
);
query.id = query.queryName;
return createDocument(queriesCollection, query)
return this.container.documentClientUtility
.createDocument(queriesCollection, query)
.then(
(savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -133,11 +131,17 @@ export class QueriesClient {
const options: any = { enableCrossPartitionQuery: true };
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
return this.container.documentClientUtility
.queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
.then(
(queryIterator: QueryIterator<ItemDefinition & Resource>) => {
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
this.container.documentClientUtility.queryDocumentsPage(
queriesCollection.id(),
queryIterator,
firstItemIndex,
options
);
return QueryUtils.queryAllPages(fetchQueries).then(
(results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
@@ -213,16 +217,17 @@ export class QueriesClient {
`Deleting query ${query.queryName}`
);
query.id = query.queryName;
const documentId = new DocumentId(
const documentId: ViewModels.DocumentId = new DocumentId(
{
partitionKey: QueriesClient.PartitionKey,
partitionKeyProperty: "id"
} as DocumentsTab,
} as ViewModels.DocumentsTab,
query,
query.queryName
); // TODO: Remove DocumentId's dependency on DocumentsTab
const options: any = { partitionKey: query.resourceId };
return deleteDocument(queriesCollection, documentId)
return this.container.documentClientUtility
.deleteDocument(queriesCollection, documentId)
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
@@ -245,10 +250,10 @@ export class QueriesClient {
}
public getResourceId(): string {
const databaseAccount = userContext.databaseAccount;
const databaseAccountName = (databaseAccount && databaseAccount.name) || "";
const subscriptionId = userContext.subscriptionId || "";
const resourceGroup = userContext.resourceGroup || "";
const databaseAccount: ViewModels.DatabaseAccount = CosmosClient.databaseAccount();
const databaseAccountName: string = (databaseAccount && databaseAccount.name) || "";
const subscriptionId: string = CosmosClient.subscriptionId() || "";
const resourceGroup: string = CosmosClient.resourceGroup() || "";
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`;
}

View File

@@ -1,81 +0,0 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
jest.mock("../DataAccessUtilityBase");
import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
import { createCollection, constructRpOptions } from "./createCollection";
import { updateUserContext } from "../../UserContext";
describe("createCollection", () => {
const createCollectionParams: CreateCollectionParams = {
createNewDatabase: false,
collectionId: "testContainer",
databaseId: "testDatabase",
databaseLevelThroughput: true,
offerThroughput: 400
};
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 createCollection(createCollectionParams);
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: {
createIfNotExists: () => {
return {
database: {
containers: {
create: () => ({})
}
}
};
}
}
});
await createCollection(createCollectionParams);
expect(client).toHaveBeenCalled();
});
it("constructRpOptions should return the correct options", () => {
expect(constructRpOptions(createCollectionParams)).toEqual({});
const manualThroughputParams: CreateCollectionParams = {
createNewDatabase: false,
collectionId: "testContainer",
databaseId: "testDatabase",
databaseLevelThroughput: false,
offerThroughput: 400
};
expect(constructRpOptions(manualThroughputParams)).toEqual({ throughput: 400 });
const autoPilotThroughputParams: CreateCollectionParams = {
createNewDatabase: false,
collectionId: "testContainer",
databaseId: "testDatabase",
databaseLevelThroughput: false,
offerThroughput: 400,
autoPilotMaxThroughput: 4000
};
expect(constructRpOptions(autoPilotThroughputParams)).toEqual({
autoscaleSettings: {
maxThroughput: 4000
}
});
});
});

View File

@@ -1,371 +0,0 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ErrorParserUtility from "../ErrorParserUtility";
import { AuthType } from "../../AuthType";
import { ContainerResponse, DatabaseResponse } from "@azure/cosmos";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import * as ARMTypes from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
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 { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
import { createDatabase } from "./createDatabase";
export const createCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
let collection: DataModels.Collection;
const clearMessage = logConsoleProgress(
`Creating a new container ${params.collectionId} for database ${params.databaseId}`
);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
databaseId: params.databaseId,
databaseLevelThroughput: params.databaseLevelThroughput,
offerThroughput: params.offerThroughput
};
await createDatabase(createDatabaseParams);
}
collection = await createCollectionWithARM(params);
} else if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
collection = await createMongoCollectionWithProxy(params);
} else {
collection = await createCollectionWithSDK(params);
}
} catch (error) {
const sanitizedError = ErrorParserUtility.replaceKnownError(JSON.stringify(error));
logConsoleError(`Error while creating container ${params.collectionId}:\n ${sanitizedError}`);
logError(JSON.stringify(error), "CreateCollection", error.code);
sendNotificationForError(error);
clearMessage();
throw error;
}
logConsoleInfo(`Successfully created container ${params.collectionId}`);
await refreshCachedResources();
clearMessage();
return collection;
};
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return createSqlContainer(params);
case DefaultAccountExperienceType.MongoDB:
return createMongoCollection(params);
case DefaultAccountExperienceType.Cassandra:
return createCassandraTable(params);
case DefaultAccountExperienceType.Graph:
return createGraph(params);
case DefaultAccountExperienceType.Table:
return createTable(params);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
};
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.SqlContainerResource = {
id: params.collectionId
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getMongoDBCollection(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.MongoDBCollectionResource = {
id: params.collectionId
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.partitionKey) {
const partitionKeyPath: string = params.partitionKey.paths[0];
resource.shardKey = { [partitionKeyPath]: "Hash" };
}
const rpPayload: ARMTypes.MongoDBCollectionCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateMongoDBCollection(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getCassandraTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.CassandraTableResource = {
id: params.collectionId
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
const rpPayload: ARMTypes.CassandraTableCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateCassandraTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getGremlinGraph(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.GremlinGraphResource = {
id: params.collectionId
};
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
const rpPayload: ARMTypes.GremlinGraphCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateGremlinGraph(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.TableResource = {
id: params.collectionId
};
const rpPayload: ARMTypes.TableCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
export const constructRpOptions = (params: DataModels.CreateDatabaseParams): ARMTypes.CreateUpdateOptions => {
if (params.databaseLevelThroughput) {
return {};
}
if (params.autoPilotMaxThroughput) {
return {
autoscaleSettings: {
maxThroughput: params.autoPilotMaxThroughput
}
};
}
return {
throughput: params.offerThroughput
};
};
const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const createCollectionBody: ContainerRequest = {
id: params.collectionId,
partitionKey: params.partitionKey || undefined,
indexingPolicy: params.indexingPolicy || undefined,
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
analyticalStorageTtl: params.analyticalStorageTtl
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
const collectionOptions: RequestOptions = {};
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
if (params.databaseLevelThroughput) {
if (params.autoPilotMaxThroughput) {
createDatabaseBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createDatabaseBody.throughput = params.offerThroughput;
}
} else {
if (params.autoPilotMaxThroughput) {
createCollectionBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createCollectionBody.throughput = params.offerThroughput;
}
}
const databaseResponse: DatabaseResponse = await client().databases.createIfNotExists(createDatabaseBody);
const collectionResponse: ContainerResponse = await databaseResponse?.database.containers.create(
createCollectionBody,
collectionOptions
);
return collectionResponse?.resource as DataModels.Collection;
};

View File

@@ -1,251 +0,0 @@
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 {
CassandraKeyspaceCreateUpdateParameters,
GremlinDatabaseCreateUpdateParameters,
MongoDBDatabaseCreateUpdateParameters,
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?.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?.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: MongoDBDatabaseCreateUpdateParameters = {
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: CassandraKeyspaceCreateUpdateParameters = {
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?.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: GremlinDatabaseCreateUpdateParameters = {
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 };
if (params.databaseLevelThroughput) {
if (params.autoPilotMaxThroughput) {
createBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createBody.throughput = params.offerThroughput;
}
}
const response: DatabaseResponse = await client().databases.create(createBody);
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

@@ -1,46 +0,0 @@
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

@@ -1,57 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,58 +0,0 @@
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

@@ -1,35 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,45 +0,0 @@
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

@@ -1,71 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,67 +0,0 @@
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

@@ -1,20 +0,0 @@
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

@@ -1,225 +0,0 @@
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

@@ -1,26 +0,0 @@
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

@@ -1,52 +0,0 @@
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 ConfigContext {
interface Config {
platform: Platform;
allowedParentFrameOrigins: RegExp;
gitSha?: string;
@@ -28,7 +28,7 @@ interface ConfigContext {
}
// Default configuration
let configContext: Readonly<ConfigContext> = {
let config: Config = {
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,58 +46,36 @@ let configContext: Readonly<ConfigContext> = {
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";
updateConfigContext({
BACKEND_ENDPOINT: "https://localhost:" + port,
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: "https://localhost:8081"
});
config.BACKEND_ENDPOINT = "https://localhost:" + port;
config.MONGO_BACKEND_ENDPOINT = "https://localhost:" + port;
config.PROXY_PATH = "/proxy";
config.EMULATOR_ENDPOINT = "https://localhost:8081";
}
export async function initializeConfiguration(): Promise<ConfigContext> {
export async function initializeConfiguration(): Promise<Config> {
try {
const response = await fetch("./config.json");
if (response.status === 200) {
try {
const externalConfig = await response.json();
Object.assign(configContext, externalConfig);
config = Object.assign({}, config, externalConfig);
} catch (error) {
console.error("Unable to parse json in config file");
console.error(error);
}
}
// Allow override of platform value with URL query parameter
// Allow override of any config value with URL query parameters
const params = new URLSearchParams(window.location.search);
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 });
}
}
params.forEach((value, key) => {
(config as any)[key] = value;
});
} catch (error) {
console.log("No configuration file found using defaults");
}
return configContext;
return config;
}
export { configContext };
export { config };

View File

@@ -153,14 +153,7 @@ export interface KeyResource {
Token: string;
}
export interface IndexingPolicy {
automatic: boolean;
indexingMode: string;
includedPaths: any;
excludedPaths: any;
compositeIndexes?: any;
spatialIndexes?: any;
}
export interface IndexingPolicy {}
export interface PartitionKey {
paths: string[];
@@ -319,6 +312,17 @@ 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;
@@ -327,24 +331,12 @@ export interface AutoPilotOfferSettings {
targetMaxThroughput?: number;
}
export interface CreateDatabaseParams {
autoPilotMaxThroughput?: number;
export interface CreateDatabaseRequest {
databaseId: string;
databaseLevelThroughput?: boolean;
offerThroughput?: number;
}
export interface CreateCollectionParams {
createNewDatabase: boolean;
collectionId: string;
databaseId: string;
databaseLevelThroughput: boolean;
offerThroughput: number;
analyticalStorageTtl?: number;
autoPilotMaxThroughput?: number;
indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey;
uniqueKeyPolicy?: UniqueKeyPolicy;
autoPilot?: AutoPilotCreationSettings;
hasAutoPilotV2FeatureFlag?: boolean;
}
export interface SharedThroughputRange {

View File

@@ -1,16 +1,41 @@
import * as DataModels from "./DataModels";
import * as monaco from "monaco-editor";
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase";
import Q from "q";
import { AccessibleVerticalList } from "../Explorer/Tree/AccessibleVerticalList";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { GitHubClient } from "../GitHub/GitHubClient";
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
import { NotebookContentItem } from "../Explorer/Notebook/NotebookContentItem";
import { QueryMetrics } from "@azure/cosmos";
import { UploadDetails } from "../workers/upload/definitions";
import Explorer from "../Explorer/Explorer";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import ConflictsTab from "../Explorer/Tabs/ConflictsTab";
import Trigger from "../Explorer/Tree/Trigger";
import DocumentId from "../Explorer/Tree/DocumentId";
import ConflictId from "../Explorer/Tree/ConflictId";
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 KernelConnectionMetadata {
name: string;
configurationEndpoints: DataModels.NotebookConfigurationEndpoints;
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
}
export interface TokenProvider {
getAuthHeader(): Promise<Headers>;
@@ -50,6 +75,11 @@ export interface WaitsForTemplate {
isTemplateReady: ko.Observable<boolean>;
}
export interface AdHocAccessData {
readWriteUrl: string;
readUrl: string;
}
export interface TreeNode {
nodeKind: string;
rid: string;
@@ -171,15 +201,118 @@ 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>;
}
/**
* 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 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;
}
/**
* Graph configuration
*/
@@ -249,6 +382,19 @@ 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;
@@ -260,10 +406,11 @@ export interface TabOptions {
tabKind: CollectionTabKind;
title: string;
tabPath: string;
documentClientUtility: DocumentClientUtilityBase;
selfLink: string;
isActive: ko.Observable<boolean>;
hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
onUpdateTabsButtons: (buttons: NavbarButtonConfig[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>;
onLoadStartKey?: number;
@@ -276,6 +423,47 @@ 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>;
@@ -303,15 +491,157 @@ 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 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,
@@ -429,8 +759,40 @@ 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 { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler";
import { MessageHandler } 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]);
}
sendCachedDataMessage(MessageTypes.LogInfo, output);
MessageHandler.sendCachedDataMessage(MessageTypes.LogInfo, output);
});
}
}
@@ -266,4 +266,4 @@ export function handleMessage(event: MessageEvent) {
}
window.addEventListener("message", handleMessage, false);
sendMessage("ready");
MessageHandler.sendMessage("ready");

View File

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

View File

@@ -13,7 +13,9 @@ 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());

View File

@@ -49,12 +49,6 @@ 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,14 +163,8 @@ exports[`Feature panel renders all flags 1`] = `
/>
<StyledCheckboxBase
checked={false}
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"
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
</Stack>
@@ -178,12 +172,6 @@ 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

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

View File

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

View File

@@ -36,8 +36,6 @@ 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

@@ -1,43 +0,0 @@
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

@@ -1,112 +0,0 @@
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

@@ -15,17 +15,15 @@ import {
} from "office-ui-fabric-react";
import * as React from "react";
import * as Logger from "../../../Common/Logger";
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { 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?: Explorer;
@@ -62,7 +60,6 @@ interface GalleryViewerComponentState {
sortBy: SortBy;
searchText: string;
dialogProps: DialogProps;
isCodeOfConductAccepted: boolean;
}
interface GalleryTabInfo {
@@ -89,7 +86,6 @@ 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;
@@ -104,8 +100,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
selectedTab: props.selectedTab,
sortBy: props.sortBy,
searchText: props.searchText,
dialogProps: undefined,
isCodeOfConductAccepted: undefined
dialogProps: undefined
};
this.sortingOptions = [
@@ -139,21 +134,10 @@ 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.createPublicGalleryTab(
GalleryTab.PublicGallery,
this.state.publicNotebooks,
this.state.isCodeOfConductAccepted
)
);
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
// 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 = {
onLinkClick: this.onPivotChange,
@@ -183,17 +167,6 @@ 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,
@@ -201,19 +174,6 @@ 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 }}>
@@ -227,12 +187,8 @@ 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>
);
@@ -298,19 +254,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) {
try {
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;
}
const response = await this.props.junoClient.getPublicNotebooks();
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");
@@ -319,8 +268,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
this.setState({
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
isCodeOfConductAccepted: this.isCodeOfConductAccepted
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
});
}
@@ -385,11 +333,12 @@ 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()];
if (item.tags) {
searchData.push(...item.tags.map(tag => tag.toUpperCase()));
}
const searchData: string[] = [
item.author.toUpperCase(),
item.description.toUpperCase(),
item.name.toUpperCase(),
...item.tags?.map(tag => tag.toUpperCase())
];
for (const data of searchData) {
if (data?.indexOf(toSearch) !== -1) {

View File

@@ -1,26 +0,0 @@
@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

@@ -1,10 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,34 +0,0 @@
// 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

@@ -1,75 +0,0 @@
// 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,8 +17,7 @@ describe("NotebookMetadataComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
views: 0
},
isFavorite: false,
downloadButtonText: "Download",
@@ -46,8 +45,7 @@ describe("NotebookMetadataComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined
views: 0
},
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 * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
@@ -19,8 +19,6 @@ import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComp
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?: Explorer;
@@ -87,17 +85,16 @@ 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 && !SessionStorageUtility.getEntry(this.props.galleryItem.id)) {
if (this.props.galleryItem) {
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 });
@@ -107,21 +104,10 @@ 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 !== undefined ? (
{this.props.backNavigationText ? (
<Link onClick={this.props.onBackClick}>
<Icon iconName="Back" /> {this.props.backNavigationText}
</Link>

View File

@@ -27,10 +27,9 @@ 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: QueriesClient;
queriesClient: ViewModels.QueriesClient;
onQuerySelect: (query: DataModels.Query) => void;
containerVisible: boolean;
saveQueryEnabled: boolean;

View File

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

View File

@@ -0,0 +1,18 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,56 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,12 @@
/*!---------------------------------------------------------
* 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

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

View File

@@ -0,0 +1,12 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,58 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,145 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,86 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,167 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,109 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,166 @@
/*!---------------------------------------------------------
* 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

@@ -0,0 +1,44 @@
<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,14 +1,12 @@
jest.mock("../../Common/DocumentClientUtilityBase");
jest.mock("../../Common/dataAccess/createCollection");
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 { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
import { CosmosClient } from "../../Common/CosmosClient";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext";
describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => {
@@ -34,8 +32,8 @@ describe("ContainerSampleGenerator", () => {
databaseId: sampleDatabaseId,
offerThroughput: 400,
databaseLevelThroughput: false,
createNewDatabase: true,
collectionId: sampleCollectionId,
rupmEnabled: false,
data: [
{
firstname: "Eva",
@@ -64,33 +62,27 @@ describe("ContainerSampleGenerator", () => {
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(DocumentClientUtility.createDocument).toHaveBeenCalled();
expect(fakeDocumentClientUtility.createDocument.called).toBe(true);
});
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());
updateUserContext({
databaseAccount: {
id: "foo",
name: "foo",
location: "foo",
type: "foo",
kind: "foo",
tags: [],
properties: {
documentEndpoint: "bar",
gremlinEndpoint: "foo",
tableEndpoint: "foo",
cassandraEndpoint: "foo"
}
}
sinon.stub(CosmosClient, "databaseAccount").returns({
properties: {}
});
const sampleCollectionId = "SampleCollection";
@@ -100,8 +92,8 @@ describe("ContainerSampleGenerator", () => {
databaseId: sampleDatabaseId,
offerThroughput: 400,
databaseLevelThroughput: false,
createNewDatabase: true,
collectionId: sampleCollectionId,
rupmEnabled: false,
data: [
"g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)"
]
@@ -117,12 +109,18 @@ describe("ContainerSampleGenerator", () => {
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(DocumentClientUtility.createDocument).toHaveBeenCalled();
expect(fakeDocumentClientUtility.createDocument.called).toBe(false);
expect(executeStub.called).toBe(true);
});

View File

@@ -1,15 +1,14 @@
import * as Constants from "../../Common/Constants";
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 * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateCollectionParams {
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
data: any[];
}
@@ -54,11 +53,18 @@ export class ContainerSampleGenerator {
}
private async createContainerAsync(): Promise<ViewModels.Collection> {
const createRequest: DataModels.CreateCollectionParams = {
const createRequest: DataModels.CreateDatabaseAndCollectionRequest = {
...this.sampleDataFile
};
await createCollection(createRequest);
const options: any = {};
if (this.container.isPreferredApiMongoDB()) {
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[Constants.HttpHeaders.supportSpatialLegacyCoordinates] = true;
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
}
await this.container.documentClientUtility.getOrCreateDatabaseAndCollection(createRequest, options);
await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) {
@@ -80,14 +86,14 @@ export class ContainerSampleGenerator {
if (!queries || queries.length < 1) {
return;
}
const account = userContext.databaseAccount;
const account = CosmosClient.databaseAccount();
const databaseId = collection.databaseId;
const gremlinClient = new GremlinClient();
gremlinClient.initialize({
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
databaseId: databaseId,
collectionId: collection.id(),
masterKey: userContext.masterKey || "",
masterKey: CosmosClient.masterKey() || "",
maxResultSize: 100
});
@@ -97,7 +103,7 @@ export class ContainerSampleGenerator {
} else {
// For SQL all queries are executed at the same time
this.sampleDataFile.data.forEach(doc => {
const subPromise = createDocument(collection, doc);
const subPromise = this.container.documentClientUtility.createDocument(collection, doc);
subPromise.catch(reason => NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, reason));
promises.push(subPromise);
});

View File

@@ -1,6 +1,6 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import Explorer from "../Explorer";

View File

@@ -15,15 +15,13 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import EnvironmentUtility from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane";
import hasher from "hasher";
import NewVertexPane from "./Panes/NewVertexPane";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import NotebookV2Tab from "./Tabs/NotebookV2Tab";
import Q from "q";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
@@ -35,10 +33,12 @@ import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import { CassandraApi } from "../Api/Apis";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { configContext, updateConfigContext } from "../ConfigContext";
import { config } from "../Config";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "../Common/CosmosClient";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter";
@@ -52,12 +52,12 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import { IGalleryItem } from "../Juno/JunoClient";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import * as Logger from "../Common/Logger";
import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler";
import { MessageHandler } from "../Common/MessageHandler";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
import { PlatformType } from "../PlatformType";
import { QueriesClient } from "../Common/QueriesClient";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
@@ -82,11 +82,6 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import { NotificationsClientBase } from "../Common/NotificationsClientBase";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -97,15 +92,6 @@ enum ShareAccessToggleState {
Read
}
interface ExplorerOptions {
notificationsClient: NotificationsClientBase;
isEmulator: boolean;
}
interface AdHocAccessData {
readWriteUrl: string;
readUrl: string;
}
export default class Explorer {
public flight: ko.Observable<string> = ko.observable<string>(
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
@@ -121,7 +107,7 @@ export default class Explorer {
public hasWriteAccess: ko.Observable<boolean>;
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public databaseAccount: ko.Observable<ViewModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
public quotaId: ko.Observable<string>;
@@ -132,7 +118,6 @@ export default class Explorer {
public isPreferredApiGraph: ko.Computed<boolean>;
public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>;
public isEmulator: boolean;
public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>;
@@ -141,8 +126,9 @@ export default class Explorer {
public extensionEndpoint: ko.Observable<string>;
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public notificationsClient: NotificationsClientBase;
public queriesClient: QueriesClient;
public documentClientUtility: DocumentClientUtilityBase;
public notificationsClient: ViewModels.NotificationsClient;
public queriesClient: ViewModels.QueriesClient;
public tableDataClient: TableDataClient;
public splitter: Splitter;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
@@ -153,7 +139,7 @@ export default class Explorer {
public isNotificationConsoleExpanded: ko.Observable<boolean>;
// Panes
public contextPanes: ContextualPaneBase[];
public contextPanes: ViewModels.ContextualPane[];
// Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>;
@@ -198,29 +184,25 @@ export default class Explorer {
public uploadItemsPane: UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: LoadQueryPane;
public saveQueryPane: ContextualPaneBase;
public saveQueryPane: ViewModels.ContextualPane;
public browseQueriesPane: BrowseQueriesPane;
public uploadFilePane: UploadFilePane;
public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ContextualPaneBase;
public gitHubReposPane: ViewModels.ContextualPane;
public publishNotebookPaneAdapter: ReactAdapter;
public copyNotebookPaneAdapter: ReactAdapter;
// features
public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<AdHocAccessData>;
public shareAccessData: ko.Observable<ViewModels.AdHocAccessData>;
public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise<void>;
public renewTokenError: ko.Observable<string>;
public tokenForRenewal: ko.Observable<string>;
@@ -246,7 +228,7 @@ export default class Explorer {
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any; // This is dynamically loaded
private _panes: ContextualPaneBase[] = [];
private _panes: ViewModels.ContextualPane[] = [];
private _importExplorerConfigComplete: boolean = false;
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = database => false;
private _isInitializingNotebooks: boolean;
@@ -269,7 +251,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(options: ExplorerOptions) {
constructor(options: ViewModels.ExplorerOptions) {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree
});
@@ -282,7 +264,7 @@ export default class Explorer {
this.deleteDatabaseText = ko.observable<string>("Delete Database");
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.databaseAccount = ko.observable<ViewModels.DatabaseAccount>();
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
SharedConstants.CollectionCreation.DefaultSubscriptionType
);
@@ -375,6 +357,7 @@ export default class Explorer {
}
});
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.documentClientUtility = options.documentClientUtility;
this.notificationsClient = options.notificationsClient;
this.isEmulator = options.isEmulator;
@@ -391,7 +374,7 @@ export default class Explorer {
this.resourceTokenPartitionKey = ko.observable<string>();
this.isAuthWithResourceToken = ko.observable<boolean>(false);
this.shareAccessData = ko.observable<AdHocAccessData>({
this.shareAccessData = ko.observable<ViewModels.AdHocAccessData>({
readWriteUrl: undefined,
readUrl: undefined
});
@@ -414,15 +397,8 @@ export default class Explorer {
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
);
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
);
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
this.canExceedMaximumValue = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
@@ -483,14 +459,8 @@ export default class Explorer {
});
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
this.defaultExperience = ko.observable<string>();
this.databaseAccount.subscribe(databaseAccount => {
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
databaseAccount
);
this.defaultExperience(defaultExperience);
updateUserContext({
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience)
});
this.databaseAccount.subscribe((databaseAccount: ViewModels.DatabaseAccount) => {
this.defaultExperience(DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount));
});
this.isPreferredApiDocumentDB = ko.computed(() => {
@@ -539,14 +509,6 @@ export default class Explorer {
return false;
});
this.isServerlessEnabled = ko.computed(
() =>
this.databaseAccount &&
this.databaseAccount()?.properties?.capabilities?.find(
item => item.name === Constants.CapabilityNames.EnableServerless
) !== undefined
);
this.isPreferredApiMongoDB = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) {
@@ -582,9 +544,8 @@ export default class Explorer {
defaultExperience &&
defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase()
) {
this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system";
};
const api = new CassandraApi();
this._isSystemDatabasePredicate = api.isSystemDatabasePredicate;
}
});
@@ -614,6 +575,7 @@ export default class Explorer {
});
this.addDatabasePane = new AddDatabasePane({
documentClientUtility: this.documentClientUtility,
id: "adddatabasepane",
visible: ko.observable<boolean>(false),
@@ -622,6 +584,7 @@ export default class Explorer {
this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
documentClientUtility: this.documentClientUtility,
id: "addcollectionpane",
visible: ko.observable<boolean>(false),
@@ -629,6 +592,7 @@ export default class Explorer {
});
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({
documentClientUtility: this.documentClientUtility,
id: "deletecollectionconfirmationpane",
visible: ko.observable<boolean>(false),
@@ -636,6 +600,7 @@ export default class Explorer {
});
this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({
documentClientUtility: this.documentClientUtility,
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
@@ -643,6 +608,7 @@ export default class Explorer {
});
this.graphStylingPane = new GraphStylingPane({
documentClientUtility: this.documentClientUtility,
id: "graphstylingpane",
visible: ko.observable<boolean>(false),
@@ -650,6 +616,7 @@ export default class Explorer {
});
this.addTableEntityPane = new AddTableEntityPane({
documentClientUtility: this.documentClientUtility,
id: "addtableentitypane",
visible: ko.observable<boolean>(false),
@@ -657,6 +624,7 @@ export default class Explorer {
});
this.editTableEntityPane = new EditTableEntityPane({
documentClientUtility: this.documentClientUtility,
id: "edittableentitypane",
visible: ko.observable<boolean>(false),
@@ -664,6 +632,7 @@ export default class Explorer {
});
this.tableColumnOptionsPane = new TableColumnOptionsPane({
documentClientUtility: this.documentClientUtility,
id: "tablecolumnoptionspane",
visible: ko.observable<boolean>(false),
@@ -671,6 +640,7 @@ export default class Explorer {
});
this.querySelectPane = new QuerySelectPane({
documentClientUtility: this.documentClientUtility,
id: "queryselectpane",
visible: ko.observable<boolean>(false),
@@ -678,6 +648,7 @@ export default class Explorer {
});
this.newVertexPane = new NewVertexPane({
documentClientUtility: this.documentClientUtility,
id: "newvertexpane",
visible: ko.observable<boolean>(false),
@@ -685,6 +656,7 @@ export default class Explorer {
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
documentClientUtility: this.documentClientUtility,
id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false),
@@ -692,6 +664,7 @@ export default class Explorer {
});
this.settingsPane = new SettingsPane({
documentClientUtility: this.documentClientUtility,
id: "settingspane",
visible: ko.observable<boolean>(false),
@@ -699,6 +672,7 @@ export default class Explorer {
});
this.executeSprocParamsPane = new ExecuteSprocParamsPane({
documentClientUtility: this.documentClientUtility,
id: "executesprocparamspane",
visible: ko.observable<boolean>(false),
@@ -706,6 +680,7 @@ export default class Explorer {
});
this.renewAdHocAccessPane = new RenewAdHocAccessPane({
documentClientUtility: this.documentClientUtility,
id: "renewadhocaccesspane",
visible: ko.observable<boolean>(false),
@@ -713,6 +688,7 @@ export default class Explorer {
});
this.uploadItemsPane = new UploadItemsPane({
documentClientUtility: this.documentClientUtility,
id: "uploaditemspane",
visible: ko.observable<boolean>(false),
@@ -722,6 +698,7 @@ export default class Explorer {
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.loadQueryPane = new LoadQueryPane({
documentClientUtility: this.documentClientUtility,
id: "loadquerypane",
visible: ko.observable<boolean>(false),
@@ -729,6 +706,7 @@ export default class Explorer {
});
this.saveQueryPane = new SaveQueryPane({
documentClientUtility: this.documentClientUtility,
id: "savequerypane",
visible: ko.observable<boolean>(false),
@@ -736,6 +714,7 @@ export default class Explorer {
});
this.browseQueriesPane = new BrowseQueriesPane({
documentClientUtility: this.documentClientUtility,
id: "browsequeriespane",
visible: ko.observable<boolean>(false),
@@ -743,6 +722,7 @@ export default class Explorer {
});
this.uploadFilePane = new UploadFilePane({
documentClientUtility: this.documentClientUtility,
id: "uploadfilepane",
visible: ko.observable<boolean>(false),
@@ -750,6 +730,7 @@ export default class Explorer {
});
this.stringInputPane = new StringInputPane({
documentClientUtility: this.documentClientUtility,
id: "stringinputpane",
visible: ko.observable<boolean>(false),
@@ -757,6 +738,7 @@ export default class Explorer {
});
this.setupNotebooksPane = new SetupNotebooksPane({
documentClientUtility: this.documentClientUtility,
id: "setupnotebookspane",
visible: ko.observable<boolean>(false),
@@ -789,6 +771,7 @@ export default class Explorer {
this.setupNotebooksPane
];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.rebindDocumentClientUtility.bind(this);
this.isTabsContentExpanded = ko.observable(false);
document.addEventListener(
@@ -870,7 +853,7 @@ export default class Explorer {
this.editTableEntityPane.title("Edit Table Entity");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new TablesAPIDataClient();
this.tableDataClient = new TablesAPIDataClient(this.documentClientUtility);
break;
case Constants.DefaultAccountExperience.Cassandra.toLowerCase():
this.addCollectionText("New Table");
@@ -889,7 +872,7 @@ export default class Explorer {
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.tableDataClient = new CassandraAPIDataClient();
this.tableDataClient = new CassandraAPIDataClient(this.documentClientUtility);
break;
}
});
@@ -975,10 +958,6 @@ export default class Explorer {
this.sparkClusterConnectionInfo.valueHasMutated();
}
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
updateUserContext({ useSDKOperations: true });
}
featureSubcription.dispose();
});
@@ -1039,7 +1018,7 @@ export default class Explorer {
);
try {
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
const databaseAccount: ViewModels.DatabaseAccount = await resourceProviderClient.patchAsync(
this.databaseAccount().id,
"2019-12-12",
{
@@ -1078,6 +1057,13 @@ export default class Explorer {
// TODO: return result
}
public rebindDocumentClientUtility(documentClientUtility: DocumentClientUtilityBase): void {
this.documentClientUtility = documentClientUtility;
this._panes.forEach((pane: ViewModels.ContextualPane) => {
pane.documentClientUtility = documentClientUtility;
});
}
public copyUrlLink(src: any, event: MouseEvent): void {
const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement;
urlLinkInput && urlLinkInput.select();
@@ -1396,7 +1382,7 @@ export default class Explorer {
}
const deferred: Q.Deferred<void> = Q.defer();
readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
this.documentClientUtility.readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
this.selectedNode(this.resourceTokenCollection());
deferred.resolve();
@@ -1420,13 +1406,16 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree
});
}
// TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer();
const offerPromise: Q.Promise<DataModels.Offer[]> = this.documentClientUtility.readOffers();
this._setLoadingStatusText("Fetching offers...");
const refreshDatabases = (offers?: DataModels.Offer[]) => {
offerPromise.then(
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
this.documentClientUtility.readDatabases(null /*options*/).then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
@@ -1477,14 +1466,6 @@ export default class Explorer {
);
}
);
};
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
this._setLoadingStatusText("Fetching offers...");
offerPromise.then(
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
refreshDatabases(offers);
},
error => {
this._setLoadingStatusText("Failed to fetch offers.");
@@ -1554,7 +1535,7 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree
});
this.isRefreshingExplorer(true);
refreshCachedResources().then(
this.documentClientUtility.refreshCachedResources().then(
() => {
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
@@ -1607,7 +1588,7 @@ export default class Explorer {
public async getArcadiaToken(): Promise<string> {
return new Promise<string>((resolve: (token: string) => void, reject: (error: any) => void) => {
sendCachedDataMessage<string>(MessageTypes.GetArcadiaToken, undefined /** params **/).then(
MessageHandler.sendCachedDataMessage<string>(MessageTypes.GetArcadiaToken, undefined /** params **/).then(
(token: string) => {
resolve(token);
},
@@ -1621,7 +1602,7 @@ export default class Explorer {
private async _getArcadiaWorkspaces(): Promise<ArcadiaWorkspaceItem[]> {
try {
const workspaces = await this._arcadiaManager.listWorkspacesAsync([userContext.subscriptionId]);
const workspaces = await this._arcadiaManager.listWorkspacesAsync([CosmosClient.subscriptionId()]);
let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length);
const sparkPromises: Promise<void>[] = [];
workspaces.forEach((workspace, i) => {
@@ -1645,11 +1626,11 @@ export default class Explorer {
}
public async createWorkspace(): Promise<string> {
return sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/);
return MessageHandler.sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/);
}
public async createSparkPool(workspaceId: string): Promise<string> {
return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]);
return MessageHandler.sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]);
}
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
@@ -1724,7 +1705,7 @@ export default class Explorer {
}
try {
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id);
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount.id);
return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default");
} catch (error) {
Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace");
@@ -1737,7 +1718,6 @@ export default class Explorer {
return;
}
let clearMessage;
try {
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
this.databaseAccount().id,
@@ -1749,14 +1729,10 @@ export default class Explorer {
notebookWorkspace.properties.status &&
notebookWorkspace.properties.status.toLowerCase() === "stopped"
) {
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
}
} catch (error) {
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning");
NotificationConsoleUtils.logConsoleError(`Failed to initialize notebook workspace: ${JSON.stringify(error)}`);
} finally {
clearMessage && clearMessage();
}
}
@@ -1831,8 +1807,8 @@ export default class Explorer {
const isRunningInPortal = window.dataExplorerPlatform == PlatformType.Portal;
const isRunningInDevMode = process.env.NODE_ENV === "development";
if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
inputs.extensionEndpoint = configContext.PROXY_PATH;
if (inputs && config.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
inputs.extensionEndpoint = config.PROXY_PATH;
}
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
@@ -1850,7 +1826,7 @@ export default class Explorer {
}
}
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
handleCachedDataMessage(message);
MessageHandler.handleCachedDataMessage(message);
return;
}
if (message.type) {
@@ -1937,7 +1913,7 @@ export default class Explorer {
this.features(inputs.features);
this.serverId(inputs.serverId);
this.extensionEndpoint(inputs.extensionEndpoint || "");
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || config.ARM_ENDPOINT));
this.notificationsClient.setExtensionEndpoint(this.extensionEndpoint());
this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType);
@@ -1953,17 +1929,11 @@ export default class Explorer {
this._importExplorerConfigComplete = true;
updateConfigContext({
ARM_ENDPOINT: this.armEndpoint()
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId
});
CosmosClient.authorizationToken(authorizationToken);
CosmosClient.masterKey(masterKey);
CosmosClient.databaseAccount(databaseAccount);
CosmosClient.subscriptionId(inputs.subscriptionId);
CosmosClient.resourceGroup(inputs.resourceGroup);
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
{
@@ -1993,7 +1963,7 @@ export default class Explorer {
return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => {
const openedSprocTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures,
tab => tab.node && tab.node.rid === storedProcedure.rid
(tab: ViewModels.Tab) => tab.node && tab.node.rid === storedProcedure.rid
);
return (
storedProcedure.rid === this.selectedNode().rid ||
@@ -2007,7 +1977,7 @@ export default class Explorer {
return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => {
const openedUdfTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions,
tab => tab.node && tab.node.rid === userDefinedFunction.rid
(tab: ViewModels.Tab) => tab.node && tab.node.rid === userDefinedFunction.rid
);
return (
userDefinedFunction.rid === this.selectedNode().rid ||
@@ -2021,7 +1991,7 @@ export default class Explorer {
return _.find(selectedCollection.triggers(), (trigger: Trigger) => {
const openedTriggerTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Triggers,
tab => tab.node && tab.node.rid === trigger.rid
(tab: ViewModels.Tab) => tab.node && tab.node.rid === trigger.rid
);
return (
trigger.rid === this.selectedNode().rid ||
@@ -2031,7 +2001,7 @@ export default class Explorer {
}
public closeAllPanes(): void {
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
this._panes.forEach((pane: ViewModels.ContextualPane) => pane.close());
}
public getPlatformType(): PlatformType {
@@ -2046,13 +2016,13 @@ export default class Explorer {
);
}
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void {
this.commandBarComponentAdapter.onUpdateTabsButtons(buttons);
}
public signInAad = () => {
TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" });
sendMessage({
MessageHandler.sendMessage({
type: MessageTypes.AadSignIn
});
};
@@ -2063,21 +2033,21 @@ export default class Explorer {
};
public clickHostedAccountSwitch = () => {
sendMessage({
MessageHandler.sendMessage({
type: MessageTypes.UpdateAccountSwitch,
click: true
});
};
public clickHostedDirectorySwitch = () => {
sendMessage({
MessageHandler.sendMessage({
type: MessageTypes.UpdateDirectoryControl,
click: true
});
};
public refreshDatabaseAccount = () => {
sendMessage({
MessageHandler.sendMessage({
type: MessageTypes.RefreshDatabaseAccount
});
};
@@ -2106,7 +2076,9 @@ export default class Explorer {
if (isNewDatabase) {
database.expandDatabase();
}
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
this.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.getDatabase().rid === database.rid
);
})
);
});
@@ -2208,8 +2180,8 @@ export default class Explorer {
return undefined;
}
const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`;
const currentActiveTab = this.tabsManager.activeTab();
const urlPrefixWithKeyParam: string = `${config.hostedExplorerURL}?key=`;
const currentActiveTab: ViewModels.Tab = this.tabsManager.activeTab();
return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`;
}
@@ -2320,7 +2292,7 @@ export default class Explorer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
}
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled";
Logger.logError(error, "Explorer/uploadFile");
@@ -2375,28 +2347,14 @@ export default class Explorer {
return Promise.resolve(false);
}
public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> {
public publishNotebook(name: string, content: string): void {
if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(
name,
content,
parentDomElement,
this.isCodeOfConductEnabled(),
this.isLinkInjectionEnabled()
);
this.notebookManager.openPublishNotebookPane(name, content);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true);
}
}
public copyNotebook(name: string, content: string): void {
if (this.notebookManager) {
this.notebookManager.openCopyNotebookPane(name, content);
this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter;
this.isCopyNotebookPaneEnabled(true);
}
}
public showOkModalDialog(title: string, msg: string): void {
this._dialogProps({
isModal: true,
@@ -2486,26 +2444,28 @@ export default class Explorer {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
const notebookTabs = this.tabsManager.getTabs(
const notebookTabs: NotebookV2Tab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
tab =>
(tab: ViewModels.Tab) =>
(tab as NotebookV2Tab).notebookPath &&
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path)
) as NotebookV2Tab[];
let notebookTab = notebookTabs && notebookTabs[0];
let notebookTab: NotebookV2Tab = notebookTabs && notebookTabs[0];
if (notebookTab) {
this.tabsManager.activateTab(notebookTab);
} else {
const options: NotebookTabOptions = {
account: userContext.databaseAccount,
const options: ViewModels.NotebookTabOptions = {
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: null,
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
documentClientUtility: null,
collection: null,
selfLink: null,
masterKey: userContext.masterKey || "",
masterKey: CosmosClient.masterKey() || "",
hashLocation: "notebooks",
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
@@ -2561,7 +2521,7 @@ export default class Explorer {
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
})
.then(newNotebookFile => {
const notebookTabs = this.tabsManager.getTabs(
const notebookTabs: ViewModels.Tab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
@@ -2649,11 +2609,7 @@ export default class Explorer {
private async _refreshNotebooksEnabledStateForAccount(): Promise<void> {
const authType = window.authType as AuthType;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
if (authType === AuthType.EncryptedToken || authType === AuthType.ResourceToken) {
this.isNotebooksEnabledForAccount(false);
return;
}
@@ -2695,7 +2651,7 @@ export default class Explorer {
}
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const subscriptionId = CosmosClient.subscriptionId();
const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
@@ -2724,7 +2680,7 @@ export default class Explorer {
};
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
const subscriptionId = userContext.subscriptionId;
const subscriptionId = CosmosClient.subscriptionId();
const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType;
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
@@ -2753,7 +2709,6 @@ export default class Explorer {
}
await this.resourceTree.initialize();
this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
}
@@ -2936,7 +2891,7 @@ export default class Explorer {
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Terminal,
tab => tab.hashLocation() == hashLocation
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation
) as TerminalTab[];
let terminalTab: TerminalTab = terminalTabs && terminalTabs[0];
@@ -2944,11 +2899,13 @@ export default class Explorer {
this.tabsManager.activateTab(terminalTab);
} else {
const newTab = new TerminalTab({
account: userContext.databaseAccount,
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.Terminal,
node: null,
title: title,
tabPath: title,
documentClientUtility: null,
collection: null,
selfLink: null,
hashLocation: hashLocation,
@@ -2970,7 +2927,7 @@ export default class Explorer {
const galleryTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Gallery,
tab => tab.hashLocation() == hashLocation
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation
);
let galleryTab = galleryTabs && galleryTabs[0];
@@ -2983,7 +2940,7 @@ export default class Explorer {
const newTab = new this.galleryTab.default({
// GalleryTabOptions
account: userContext.databaseAccount,
account: CosmosClient.databaseAccount(),
container: this,
junoClient: this.notebookManager?.junoClient,
notebookUrl,
@@ -3016,21 +2973,24 @@ export default class Explorer {
const notebookViewerTabModule = this.notebookViewerTab;
let isNotebookViewerOpen = (tab: TabsBase) => {
let isNotebookViewerOpen = (tab: ViewModels.Tab) => {
const notebookViewerTab = tab as typeof notebookViewerTabModule.default;
return notebookViewerTab.notebookUrl === notebookUrl;
};
const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, tab => {
const notebookViewerTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: ViewModels.Tab) => {
return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab);
});
}
);
let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0];
if (notebookViewerTab) {
this.tabsManager.activateNewTab(notebookViewerTab);
} else {
notebookViewerTab = new this.notebookViewerTab.default({
account: userContext.databaseAccount,
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.NotebookViewer,
node: null,
title: title,

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 * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab";
import { GraphExplorer } from "./GraphExplorer";
import * as Constants from "../../../Common/Constants";

View File

@@ -1,4 +1,3 @@
jest.mock("../../../Common/DocumentClientUtilityBase");
import React from "react";
import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme";
@@ -8,11 +7,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", () => {
@@ -87,31 +86,13 @@ 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 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 });
it("should error if id is not a string", () => {
const doc = createFakeDoc({ id: { foo: 1 } });
try {
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
expect(true).toBe(false);
@@ -120,8 +101,16 @@ describe("getPkIdFromDocumentId", () => {
}
});
it("should error if pk is empty array", () => {
let doc = createFakeDoc({ mypk: [] });
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: [] });
try {
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
expect(true).toBe(false);
@@ -145,7 +134,7 @@ describe("GraphExplorer", () => {
const COLLECTION_SELF_LINK = "collectionSelfLink";
const gremlinRU = 789.12;
const createMockProps = (): GraphExplorerProps => {
const createMockProps = (documentClientUtility?: any): GraphExplorerProps => {
const graphConfig = GraphTab.createGraphConfig();
const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig);
@@ -160,6 +149,7 @@ describe("GraphExplorer", () => {
onIsValidQueryChange: (isValidQuery: boolean): void => {},
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
documentClientUtility: documentClientUtility,
collectionRid: COLLECTION_RID,
collectionSelfLink: COLLECTION_SELF_LINK,
graphBackendEndpoint: "graphBackendEndpoint",
@@ -198,6 +188,7 @@ 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;
@@ -224,6 +215,46 @@ 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,
@@ -302,29 +333,7 @@ describe("GraphExplorer", () => {
done: any,
ignoreD3Update: boolean
): GraphExplorer => {
(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();
const props: GraphExplorerProps = createMockProps(createDocumentClientUtilityMock(docDBResponse));
wrapper = mount(<GraphExplorer {...props} />);
graphExplorerInstance = wrapper.instance() as GraphExplorer;
setupMocks(graphExplorerInstance, backendResponses, done, ignoreD3Update);
@@ -332,7 +341,7 @@ describe("GraphExplorer", () => {
};
const cleanUpStubsWrapper = () => {
jest.resetAllMocks();
queryDocStub.restore();
connectStub.restore();
submitToBackendSpy.restore();
renderResultAsJsonStub.restore();
@@ -369,11 +378,22 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
});
it("should submit g.V() as docdb query with proper parameters", () => {
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: true
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 });
});
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {
@@ -406,11 +426,22 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
});
it("should submit g.V() as docdb query with proper parameters", () => {
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: true
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 });
});
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 * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { 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 { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
export interface GraphAccessor {
applyFilter: () => void;
@@ -47,6 +47,7 @@ export interface GraphExplorerProps {
onIsValidQueryChange: (isValidQuery: boolean) => void;
collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
@@ -696,6 +697,7 @@ 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);
@@ -728,12 +730,14 @@ 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 queryDocuments(this.props.databaseId, this.props.collectionId, query, {
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(
})
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
return iterator.fetchNext().then(response => response.resources);
},
@@ -1371,7 +1375,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
let pk = (d as any)[collectionPartitionKeyProperty];
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
if (typeof pk !== "string") {
if (Array.isArray(pk) && pk.length > 0) {
// pk is [{ id: 'id', _value: 'value' }]
pk = pk[0]["_value"];
@@ -1728,9 +1732,11 @@ 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 queryDocuments(this.props.databaseId, this.props.collectionId, query, {
return this.props.documentClientUtility
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
})
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
@@ -1760,7 +1766,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return queryDocumentsPage(
return this.props.documentClientUtility
.queryDocumentsPage(
this.props.collectionRid,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,

View File

@@ -3,6 +3,7 @@ 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;
@@ -17,6 +18,7 @@ interface Parameter {
graphConfig?: GraphConfig;
collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
@@ -49,6 +51,7 @@ 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}

View File

@@ -1,6 +1,6 @@
import * as sinon from "sinon";
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as Logger from "../../../Common/Logger";
describe("Gremlin Client", () => {

View File

@@ -4,7 +4,7 @@
import * as Q from "q";
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../../Common/HashMap";
import * as Logger from "../../../Common/Logger";

View File

@@ -12,12 +12,11 @@ import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/Com
import { StyleConstants } from "../../../Common/Constants";
import { CommandBarUtil } from "./CommandBarUtil";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
public container: Explorer;
private tabsButtons: CommandButtonComponentProps[];
private tabsButtons: ViewModels.NavbarButtonConfig[];
private isNotebookTabActive: ko.Computed<boolean>;
constructor(container: Explorer) {
@@ -45,15 +44,14 @@ export class CommandBarComponentAdapter implements ReactAdapter {
container.isHostedDataExplorerEnabled,
container.isSynapseLinkUpdating,
container.databaseAccount,
this.isNotebookTabActive,
container.isServerlessEnabled
this.isNotebookTabActive
];
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
this.parameters = ko.observable(Date.now());
}
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void {
this.tabsButtons = buttons;
this.triggerRender();
}

View File

@@ -7,47 +7,6 @@ import Explorer from "../../Explorer";
describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer;
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link (Preview)";
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = () => false;
});
it("Account is not serverless - button should be visible", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const enableAzureSynapseLinkBtn = buttons.find(
button => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeDefined();
});
it("Account is serverless - button should be hidden", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const enableAzureSynapseLinkBtn = buttons.find(
button => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeUndefined();
});
});
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
@@ -64,7 +23,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
it("Notebooks is already enabled - button should be hidden", () => {
@@ -128,7 +86,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -210,7 +167,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -298,7 +254,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -350,7 +305,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
it("should only show New SQL Query and Open Query buttons", () => {

View File

@@ -24,20 +24,19 @@ import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import GitHubIcon from "../../../../images/github.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import { configContext, Platform } from "../../../ConfigContext";
import { config, Platform } from "../../../Config";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentButtonFactory {
private static counter: number = 0;
public static createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
public static createStaticCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
if (container.isAuthWithResourceToken()) {
return CommandBarComponentButtonFactory.createStaticCommandBarButtonsForResourceToken(container);
}
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
const buttons: ViewModels.NavbarButtonConfig[] = [newCollectionBtn];
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
@@ -113,7 +112,7 @@ export class CommandBarComponentButtonFactory {
if (CommandBarComponentButtonFactory.areScriptsSupported(container)) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
@@ -134,12 +133,12 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
public static createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
public static createContextCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
const buttons: ViewModels.NavbarButtonConfig[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
const label = "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
const newMongoShellBtn: ViewModels.NavbarButtonConfig = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
@@ -157,15 +156,15 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
public static createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
public static createControlCommandBarButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
const buttons: ViewModels.NavbarButtonConfig[] = [];
if (window.dataExplorerPlatform === PlatformType.Hosted) {
return buttons;
}
if (!container.isPreferredApiCassandra()) {
const label = "Settings";
const settingsPaneButton: CommandButtonComponentProps = {
const settingsPaneButton: ViewModels.NavbarButtonConfig = {
iconSrc: SettingsIcon,
iconAlt: label,
onCommandClick: () => container.settingsPane.open(),
@@ -180,7 +179,7 @@ export class CommandBarComponentButtonFactory {
if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
const fullScreenButton: ViewModels.NavbarButtonConfig = {
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => container.generateSharedAccessData(),
@@ -196,7 +195,7 @@ export class CommandBarComponentButtonFactory {
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
const feedbackButtonOptions: ViewModels.NavbarButtonConfig = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: () => container.provideFeedbackEmail(),
@@ -212,7 +211,7 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
public static createDivider(): CommandButtonComponentProps {
public static createDivider(): ViewModels.NavbarButtonConfig {
const label = `divider${CommandBarComponentButtonFactory.counter++}`;
return {
isDivider: true,
@@ -229,7 +228,7 @@ export class CommandBarComponentButtonFactory {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
}
private static createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
private static createNewCollectionGroup(container: Explorer): ViewModels.NavbarButtonConfig {
const label = container.addCollectionText();
return {
iconSrc: AddCollectionIcon,
@@ -242,15 +241,10 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
private static createOpenSynapseLinkDialogButton(container: Explorer): ViewModels.NavbarButtonConfig {
if (config.platform === Platform.Emulator) {
return null;
}
if (container.isServerlessEnabled()) {
return null;
}
if (
container.databaseAccount &&
container.databaseAccount() &&
@@ -282,7 +276,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNewDatabase(container: Explorer): CommandButtonComponentProps {
private static createNewDatabase(container: Explorer): ViewModels.NavbarButtonConfig {
const label = container.addDatabaseText();
return {
iconSrc: AddDatabaseIcon,
@@ -297,7 +291,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
private static createNewSQLQueryButton(container: Explorer): ViewModels.NavbarButtonConfig {
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
const label = "New SQL Query";
return {
@@ -331,15 +325,15 @@ export class CommandBarComponentButtonFactory {
return null;
}
public static createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
public static createScriptCommandButtons(container: Explorer): ViewModels.NavbarButtonConfig[] {
const buttons: ViewModels.NavbarButtonConfig[] = [];
const shouldEnableScriptsCommands: boolean =
!container.isDatabaseNodeOrNoneSelected() && CommandBarComponentButtonFactory.areScriptsSupported(container);
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
@@ -356,7 +350,7 @@ export class CommandBarComponentButtonFactory {
if (shouldEnableScriptsCommands) {
const label = "New UDF";
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
const newUserDefinedFunctionBtn: ViewModels.NavbarButtonConfig = {
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
@@ -373,7 +367,7 @@ export class CommandBarComponentButtonFactory {
if (shouldEnableScriptsCommands) {
const label = "New Trigger";
const newTriggerBtn: CommandButtonComponentProps = {
const newTriggerBtn: ViewModels.NavbarButtonConfig = {
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
@@ -391,7 +385,7 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
private static createScaleAndSettingsButton(container: Explorer): ViewModels.NavbarButtonConfig {
let isShared = false;
if (container.isDatabaseNodeSelected()) {
isShared = container.findSelectedDatabase().isDatabaseShared();
@@ -416,7 +410,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
private static createNewNotebookButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "New Notebook";
return {
iconSrc: NewNotebookIcon,
@@ -429,7 +423,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
private static createuploadNotebookButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
@@ -442,7 +436,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
private static createOpenQueryButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
@@ -455,7 +449,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
private static createOpenQueryFromDiskButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
@@ -468,8 +462,8 @@ export class CommandBarComponentButtonFactory {
};
}
private static createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
private static createEnableNotebooksButton(container: Explorer): ViewModels.NavbarButtonConfig {
if (config.platform === Platform.Emulator) {
return null;
}
const label = "Enable Notebooks (Preview)";
@@ -489,7 +483,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
private static createOpenTerminalButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
@@ -502,7 +496,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
private static createOpenMongoTerminalButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "Open Mongo Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
@@ -528,7 +522,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
private static createOpenCassandraTerminalButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "Open Cassandra Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
@@ -554,7 +548,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
private static createNotebookWorkspaceResetButton(container: Explorer): ViewModels.NavbarButtonConfig {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
@@ -567,7 +561,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
private static createManageGitHubAccountButton(container: Explorer): ViewModels.NavbarButtonConfig {
let connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
return {
@@ -590,7 +584,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
private static createStaticCommandBarButtonsForResourceToken(container: Explorer): ViewModels.NavbarButtonConfig[] {
const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container);
const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);

View File

@@ -1,10 +1,9 @@
import { CommandBarUtil } from "./CommandBarUtil";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
describe("CommandBarUtil tests", () => {
const createButton = (): CommandButtonComponentProps => {
const createButton = (): ViewModels.NavbarButtonConfig => {
return {
iconSrc: "icon",
iconAlt: "label",
@@ -55,7 +54,7 @@ describe("CommandBarUtil tests", () => {
});
it("should create buttons with unique keys", () => {
const btns: CommandButtonComponentProps[] = [];
const btns: ViewModels.NavbarButtonConfig[] = [];
for (let i = 0; i < 5; i++) {
btns.push(createButton());
}

View File

@@ -1,11 +1,12 @@
import _ from "underscore";
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Observable } from "knockout";
import { IconType } from "office-ui-fabric-react/lib/Icon";
import { IComponentAsProps } from "office-ui-fabric-react/lib/Utilities";
import { StyleConstants } from "../../../Common/Constants";
import { KeyCodes, StyleConstants } from "../../../Common/Constants";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { Dropdown, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { Dropdown, DropdownMenuItemType, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
@@ -20,13 +21,13 @@ export class CommandBarUtil {
* Convert our NavbarButtonConfig to UI Fabric buttons
* @param btns
*/
public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
public static convertButton(btns: ViewModels.NavbarButtonConfig[], backgroundColor: string): ICommandBarItemProps[] {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
return btns
.filter(btn => btn)
.map(
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
(btn: ViewModels.NavbarButtonConfig, index: number): ICommandBarItemProps => {
if (btn.isDivider) {
return CommandBarUtil.createDivider(btn.commandButtonLabel);
}

View File

@@ -3,19 +3,17 @@
*/
import * as React from "react";
import {
CommandButtonComponent,
CommandButtonComponentProps
} from "../../Controls/CommandButton/CommandButtonComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandButtonComponent } from "../../Controls/CommandButton/CommandButtonComponent";
export interface ControlBarComponentProps {
buttons: CommandButtonComponentProps[];
buttons: ViewModels.NavbarButtonConfig[];
}
export class ControlBarComponent extends React.Component<ControlBarComponentProps> {
private static renderButtons(commandButtonOptions: CommandButtonComponentProps[]): JSX.Element[] {
private static renderButtons(commandButtonOptions: ViewModels.NavbarButtonConfig[]): JSX.Element[] {
return commandButtonOptions.map(
(btn: CommandButtonComponentProps, index: number): JSX.Element => {
(btn: ViewModels.NavbarButtonConfig, index: number): JSX.Element => {
// Remove label
btn.commandButtonLabel = null;
return CommandButtonComponent.renderButton(btn, `${index}`);

View File

@@ -8,12 +8,12 @@ import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { ControlBarComponent } from "./ControlBarComponent";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
export class ControlBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private buttons: ko.ObservableArray<CommandButtonComponentProps>) {
constructor(private buttons: ko.ObservableArray<ViewModels.NavbarButtonConfig>) {
this.buttons.subscribe(() => this.forceRender());
this.parameters = ko.observable<number>(Date.now());
}

View File

@@ -8,7 +8,6 @@ import { actions, createContentRef, createKernelRef, selectors } from "@nteract/
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
import { NotebookContentItem } from "../NotebookContentItem";
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
import { CdbAppState } from "./types";
export interface NotebookComponentAdapterOptions {
contentItem: NotebookContentItem;
@@ -19,7 +18,6 @@ export interface NotebookComponentAdapterOptions {
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
private onUpdateKernelInfo: () => void;
public getNotebookParentElement: () => HTMLElement;
public parameters: any;
constructor(options: NotebookComponentAdapterOptions) {
@@ -46,11 +44,6 @@ export class NotebookComponentAdapter extends NotebookComponentBootstrapper impl
})
);
}
this.getNotebookParentElement = () => {
const cdbAppState = this.getStore().getState() as CdbAppState;
return cdbAppState.cdb.currentNotebookParentElements.get(this.contentRef);
};
}
protected renderExtraComponent = (): JSX.Element => {

View File

@@ -17,7 +17,7 @@ import {
} from "@nteract/core";
import * as Immutable from "immutable";
import { Provider } from "react-redux";
import { CellType, CellId, ImmutableNotebook } from "@nteract/commutable";
import { CellType, CellId, toJS } from "@nteract/commutable";
import { Store, AnyAction } from "redux";
import "./NotebookComponent.less";
@@ -71,14 +71,14 @@ export class NotebookComponentBootstrapper {
);
}
public getContent(): { name: string; content: string | ImmutableNotebook } {
public getContent(): { name: string; content: string } {
const record = this.getStore()
.getState()
.core.entities.contents.byRef.get(this.contentRef);
let content: string | ImmutableNotebook;
let content: string;
switch (record.model.type) {
case "notebook":
content = record.model.notebook;
content = JSON.stringify(toJS(record.model.notebook));
break;
case "file":
content = record.model.text;
@@ -98,7 +98,7 @@ export class NotebookComponentBootstrapper {
actions.fetchContentFulfilled({
filepath: undefined,
model: NotebookComponentBootstrapper.wrapModelIntoContent(name, undefined, content),
kernelRef: undefined, // must be undefined or it will be auto-started by the epic
kernelRef: createKernelRef(),
contentRef: this.contentRef
})
);

View File

@@ -84,22 +84,3 @@ export const traceNotebookTelemetry = (payload: {
payload
};
};
export const UPDATE_NOTEBOOK_PARENT_DOM_ELTS = "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
export interface UpdateNotebookParentDomEltAction {
type: "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
};
}
export const UpdateNotebookParentDomElt = (payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
}): UpdateNotebookParentDomEltAction => {
return {
type: UPDATE_NOTEBOOK_PARENT_DOM_ELTS,
payload
};
};

View File

@@ -1,13 +1,13 @@
import * as Immutable from "immutable";
import { ActionsObservable, StateObservable } from "redux-observable";
import { Subject, empty } from "rxjs";
import { Subject } from "rxjs";
import { toArray } from "rxjs/operators";
import { makeNotebookRecord } from "@nteract/commutable";
import { actions, state } from "@nteract/core";
import * as sinon from "sinon";
import { CdbAppState, makeCdbRecord } from "./types";
import { launchWebSocketKernelEpic, autoStartKernelEpic } from "./epics";
import { launchWebSocketKernelEpic } from "./epics";
import { NotebookUtil } from "../NotebookUtil";
import { sessions } from "rx-jupyter";
@@ -74,6 +74,11 @@ describe("Extract kernel from notebook", () => {
});
});
describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef";
const kernelRef = "fake";
const initialState = {
app: state.makeAppRecord({
host: state.makeJupyterHostRecord({
@@ -110,12 +115,6 @@ const initialState = {
})
};
describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef";
const kernelRef = "fake";
it("launches remote kernels", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
@@ -491,55 +490,3 @@ describe("launchWebSocketKernelEpic", () => {
});
});
});
describe("autoStartKernelEpic", () => {
const contentRef = "fakeContentRef";
const kernelRef = "fake";
it("automatically starts kernel when content fetch is successful if kernelRef is defined", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.fetchContentFulfilled({
contentRef,
kernelRef,
filepath: "filepath",
model: {}
})
);
const responseActions = await autoStartKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(responseActions).toMatchObject([
{
type: actions.RESTART_KERNEL,
payload: {
contentRef,
kernelRef,
outputHandling: "None"
}
}
]);
});
it("Don't start kernel when content fetch is successful if kernelRef is not defined", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.fetchContentFulfilled({
contentRef,
kernelRef: undefined,
filepath: "filepath",
model: {}
})
);
const responseActions = await autoStartKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(responseActions).toMatchObject([]);
});
});

View File

@@ -1,4 +1,4 @@
import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { empty, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { webSocket } from "rxjs/webSocket";
import { ActionsObservable, StateObservable } from "redux-observable";
import { ofType } from "redux-observable";
@@ -34,7 +34,7 @@ import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable";
import * as Constants from "../../../Common/Constants";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
@@ -77,7 +77,7 @@ const addInitialCodeCellEpic = (
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return EMPTY;
return empty();
}
const cellOrder = selectors.notebook.cellOrder(model);
@@ -90,40 +90,7 @@ const addInitialCodeCellEpic = (
);
}
return EMPTY;
})
);
};
/**
* Automatically start kernel if kernelRef is present.
* The kernel is normally lazy-started when a cell is being executed, but a running kernel is
* required for code completion to work.
* For notebook viewer, there is no kernel
* @param action$
* @param state$
*/
export const autoStartKernelEpic = (
action$: ActionsObservable<actions.FetchContentFulfilled>,
state$: StateObservable<AppState>
): Observable<{} | actions.CreateCellBelow> => {
return action$.pipe(
ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap(action => {
const state = state$.value;
const { contentRef, kernelRef } = action.payload;
if (!kernelRef) {
return EMPTY;
}
return of(
actions.restartKernel({
contentRef,
kernelRef,
outputHandling: "None"
})
);
return empty();
})
);
};
@@ -321,7 +288,7 @@ export const launchWebSocketKernelEpic = (
const state = state$.value;
const host = selectors.currentHost(state);
if (host.type !== "jupyter") {
return EMPTY;
return empty();
}
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
serverConfig.userPuid = getUserPuid();
@@ -332,7 +299,7 @@ export const launchWebSocketKernelEpic = (
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") {
return EMPTY;
return empty();
}
let kernelSpecToLaunch = kernelSpecName;
@@ -546,26 +513,26 @@ const changeWebSocketKernelEpic = (
const state = state$.value;
const host = selectors.currentHost(state);
if (host.type !== "jupyter") {
return EMPTY;
return empty();
}
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
if (!oldKernelRef) {
return EMPTY;
return empty();
}
const oldKernel = selectors.kernel(state, { kernelRef: oldKernelRef });
if (!oldKernel || oldKernel.type !== "websocket") {
return EMPTY;
return empty();
}
const { sessionId } = oldKernel;
if (!sessionId) {
return EMPTY;
return empty();
}
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") {
return EMPTY;
return empty();
}
const {
filepath,
@@ -626,7 +593,7 @@ const focusInitialCodeCellEpic = (
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return EMPTY;
return empty();
}
const cellOrder = selectors.notebook.cellOrder(model);
@@ -641,7 +608,7 @@ const focusInitialCodeCellEpic = (
);
}
return EMPTY;
return empty();
})
);
};
@@ -694,7 +661,7 @@ const notificationsToUserEpic = (
break;
}
}
return EMPTY;
return empty();
})
);
};
@@ -734,7 +701,7 @@ const handleKernelConnectionLostEpic = (
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
return of(EMPTY);
return of(empty());
}
return concat(
@@ -847,7 +814,7 @@ const closeUnsupportedMimetypesEpic = (
explorer.showOkModalDialog("File cannot be rendered", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
}
return EMPTY;
return empty();
})
);
};
@@ -875,14 +842,13 @@ const closeContentFailedToFetchEpic = (
explorer.showOkModalDialog("Failure to load", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
}
return EMPTY;
return empty();
})
);
};
export const allEpics = [
addInitialCodeCellEpic,
autoStartKernelEpic,
focusInitialCodeCellEpic,
notificationsToUserEpic,
launchWebSocketKernelEpic,

View File

@@ -82,19 +82,6 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
});
return state;
}
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
var parentEltsMap = state.get("currentNotebookParentElements");
const contentRef = typedAction.payload.contentRef;
const parentElt = typedAction.payload.parentElt;
if (parentElt) {
parentEltsMap.set(contentRef, parentElt);
} else {
parentEltsMap.delete(contentRef);
}
return state.set("currentNotebookParentElements", parentEltsMap);
}
}
return state;
};

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