mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-03 00:01:42 +00:00
Compare commits
40 Commits
users/srna
...
replace-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f895d343 | ||
|
|
92073a5646 | ||
|
|
3bc2701356 | ||
|
|
35dbaeea96 | ||
|
|
18745a9ae6 | ||
|
|
5c84b3a7d4 | ||
|
|
3223ff7685 | ||
|
|
38732af907 | ||
|
|
e837f574a8 | ||
|
|
47a5c315b5 | ||
|
|
1c80ced259 | ||
|
|
5e6ac78b7d | ||
|
|
999196193f | ||
|
|
951289e190 | ||
|
|
3279460cfd | ||
|
|
07b9c1d1b7 | ||
|
|
dde2ca75c4 | ||
|
|
f44a3da568 | ||
|
|
22b2e1df48 | ||
|
|
2752d6af00 | ||
|
|
5be6f982f9 | ||
|
|
4fc9393b76 | ||
|
|
cb5fe5316e | ||
|
|
c0ce637eec | ||
|
|
b61a235bf6 | ||
|
|
0fa97c2ce9 | ||
|
|
fb71fb4e82 | ||
|
|
455722c316 | ||
|
|
5886db81e9 | ||
|
|
7a3e54d43e | ||
|
|
3051961093 | ||
|
|
abce15a6b2 | ||
|
|
a5b824ebb5 | ||
|
|
e28765d740 | ||
|
|
95f1efc03f | ||
|
|
455a6ac81b | ||
|
|
08ee86ecf1 | ||
|
|
0011007d5f | ||
|
|
ee51e873b8 | ||
|
|
206a8ef93b |
@@ -41,6 +41,7 @@ module.exports = {
|
|||||||
"@typescript-eslint/no-extraneous-class": "error",
|
"@typescript-eslint/no-extraneous-class": "error",
|
||||||
"no-null/no-null": "error",
|
"no-null/no-null": "error",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }]
|
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
|
||||||
|
eqeqeq: "error"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -134,6 +134,11 @@ jobs:
|
|||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
name: videos
|
||||||
|
if: ${{ failure() }}
|
||||||
|
with:
|
||||||
|
path: "**/*.mp4"
|
||||||
endtoendmongo:
|
endtoendmongo:
|
||||||
name: "End To End Tests | Mongo"
|
name: "End To End Tests | Mongo"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
@@ -163,6 +168,11 @@ jobs:
|
|||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: ${{ failure() }}
|
||||||
|
name: videos
|
||||||
|
with:
|
||||||
|
path: "**/*.mp4"
|
||||||
accessibility:
|
accessibility:
|
||||||
name: "Accessibility | Hosted"
|
name: "Accessibility | Hosted"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"pluginsFile": false,
|
"pluginsFile": false,
|
||||||
"fixturesFolder": false,
|
"fixturesFolder": false,
|
||||||
"supportFile": "./support/index.js",
|
"supportFile": "./support/index.js",
|
||||||
"defaultCommandTimeout": 60000,
|
"defaultCommandTimeout": 90000,
|
||||||
"chromeWebSecurity": false,
|
"chromeWebSecurity": false,
|
||||||
"reporter": "mochawesome",
|
"reporter": "mochawesome",
|
||||||
"reporterOptions": {
|
"reporterOptions": {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "cypress run",
|
"test": "cypress run",
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||||
"test:sql": "cypress run --browser chrome --headless --spec \"./integration/dataexplorer/SQL/*\"",
|
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
||||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
|
"test: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"
|
"test:debug": "cypress open"
|
||||||
},
|
},
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -5,13 +5,14 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/cosmos": {
|
"@azure/cosmos": {
|
||||||
"version": "3.7.4",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.9.0.tgz",
|
||||||
"integrity": "sha512-IbSEadapQDajSCXj7gUc8OklkOd/oAY4w7XBLHouWc4iKQTtntb2DmGjhrbh2W5Ku+pmBSr1GTApCjQ55iIjlQ==",
|
"integrity": "sha512-SA+QB54I8Dvg/ZolHpsEDLK/sbSB9sFmSU1ElnMTFw88TVik+LYHq4o/srU2TY6Gr1BketjPmgLVEqrmnRvjkw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/debug": "^4.1.4",
|
"@types/debug": "^4.1.4",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"jsbi": "^3.1.3",
|
||||||
"node-abort-controller": "^1.0.4",
|
"node-abort-controller": "^1.0.4",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"os-name": "^3.1.0",
|
"os-name": "^3.1.0",
|
||||||
@@ -22,14 +23,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
|
||||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "8.2.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
||||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
|
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -10132,9 +10133,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"canvas": {
|
"canvas": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
|
||||||
"integrity": "sha512-bEO9f1ThmbknLPxCa8Es7obPlN9W3stB1bo7njlhOFKIdUTldeTqXCh9YclCPAi2pSQs84XA0jq/QEZXSzgyMw==",
|
"integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"nan": "^2.14.0",
|
"nan": "^2.14.0",
|
||||||
"node-pre-gyp": "^0.11.0",
|
"node-pre-gyp": "^0.11.0",
|
||||||
@@ -20204,6 +20205,11 @@
|
|||||||
"esprima": "^4.0.0"
|
"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": {
|
"jsbn": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||||
@@ -21534,9 +21540,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"needle": {
|
"needle": {
|
||||||
"version": "2.4.1",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz",
|
||||||
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
|
"integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "^3.2.6",
|
"debug": "^3.2.6",
|
||||||
"iconv-lite": "^0.4.4",
|
"iconv-lite": "^0.4.4",
|
||||||
@@ -24630,9 +24636,9 @@
|
|||||||
"integrity": "sha1-ZfDBWZNSs1Ny7KrlolDmEHN27Wk="
|
"integrity": "sha1-ZfDBWZNSs1Ny7KrlolDmEHN27Wk="
|
||||||
},
|
},
|
||||||
"simple-concat": {
|
"simple-concat": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
|
||||||
},
|
},
|
||||||
"simple-get": {
|
"simple-get": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/cosmos": "3.7.4",
|
"@azure/cosmos": "3.9.0",
|
||||||
"@azure/cosmos-language-service": "0.0.4",
|
"@azure/cosmos-language-service": "0.0.4",
|
||||||
"@jupyterlab/services": "4.2.0",
|
"@jupyterlab/services": "4.2.0",
|
||||||
"@jupyterlab/terminal": "1.2.1",
|
"@jupyterlab/terminal": "1.2.1",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
"canvas": "2.6.0",
|
"canvas": "2.6.1",
|
||||||
"clean-webpack-plugin": "0.1.19",
|
"clean-webpack-plugin": "0.1.19",
|
||||||
"copy-webpack-plugin": "6.0.2",
|
"copy-webpack-plugin": "6.0.2",
|
||||||
"crossroads": "0.12.2",
|
"crossroads": "0.12.2",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AutopilotTier } from "../Contracts/DataModels";
|
import { AutopilotTier } from "../Contracts/DataModels";
|
||||||
import { config } from "../Config";
|
import { configContext } from "../ConfigContext";
|
||||||
import { HashMap } from "./HashMap";
|
import { HashMap } from "./HashMap";
|
||||||
|
|
||||||
export class AuthorizationEndpoints {
|
export class AuthorizationEndpoints {
|
||||||
@@ -7,14 +7,23 @@ export class AuthorizationEndpoints {
|
|||||||
public static common: string = "https://login.windows.net/";
|
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 {
|
export class BackendEndpoints {
|
||||||
public static localhost: string = "https://localhost:12900";
|
public static localhost: string = "https://localhost:12900";
|
||||||
public static dev: string = "https://ext.documents-dev.windows-int.net";
|
public static dev: string = "https://ext.documents-dev.windows-int.net";
|
||||||
public static productionPortal: string = config.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
|
public static productionPortal: string = configContext.BACKEND_ENDPOINT || "https://main.documentdb.ext.azure.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EndpointsRegex {
|
export class EndpointsRegex {
|
||||||
public static readonly cassandra = "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com";
|
public static readonly cassandra = [
|
||||||
|
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||||
|
"HostName=(.*).cassandra.cosmos.azure.com"
|
||||||
|
];
|
||||||
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
||||||
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
||||||
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
||||||
@@ -113,6 +122,8 @@ export class Features {
|
|||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
public static readonly enableNotebooks = "enablenotebooks";
|
public static readonly enableNotebooks = "enablenotebooks";
|
||||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||||
|
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||||
|
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
public static readonly livyEndpoint = "livyendpoint";
|
||||||
public static readonly notebookServerUrl = "notebookserverurl";
|
public static readonly notebookServerUrl = "notebookserverurl";
|
||||||
@@ -123,6 +134,7 @@ export class Features {
|
|||||||
public static readonly enableAutoPilotV2 = "enableautopilotv2";
|
public static readonly enableAutoPilotV2 = "enableautopilotv2";
|
||||||
public static readonly ttl90Days = "ttl90days";
|
public static readonly ttl90Days = "ttl90days";
|
||||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||||
|
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AfecFeatures {
|
export class AfecFeatures {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CosmosClient, tokenProvider, endpoint, requestPlugin, getTokenFromAuthService } from "./CosmosClient";
|
|
||||||
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
|
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
|
||||||
import { config, Platform } from "../Config";
|
import { configContext, Platform, updateConfigContext, resetConfigContext } from "../ConfigContext";
|
||||||
|
import { updateUserContext } from "../UserContext";
|
||||||
|
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
|
||||||
|
|
||||||
describe("tokenProvider", () => {
|
describe("tokenProvider", () => {
|
||||||
const options = {
|
const options = {
|
||||||
@@ -32,7 +33,9 @@ describe("tokenProvider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not call the auth service if a master key is set", async () => {
|
it("does not call the auth service if a master key is set", async () => {
|
||||||
CosmosClient.masterKey("foo");
|
updateUserContext({
|
||||||
|
masterKey: "foo"
|
||||||
|
});
|
||||||
await tokenProvider(options);
|
await tokenProvider(options);
|
||||||
expect((window.fetch as any).mock.calls.length).toBe(0);
|
expect((window.fetch as any).mock.calls.length).toBe(0);
|
||||||
});
|
});
|
||||||
@@ -41,7 +44,7 @@ describe("tokenProvider", () => {
|
|||||||
describe("getTokenFromAuthService", () => {
|
describe("getTokenFromAuthService", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete window.dataExplorer;
|
delete window.dataExplorer;
|
||||||
delete config.BACKEND_ENDPOINT;
|
resetConfigContext();
|
||||||
window.fetch = jest.fn().mockImplementation(() => {
|
window.fetch = jest.fn().mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
json: () => "{}",
|
json: () => "{}",
|
||||||
@@ -64,7 +67,9 @@ describe("getTokenFromAuthService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL in dev", () => {
|
it("builds the correct URL in dev", () => {
|
||||||
config.BACKEND_ENDPOINT = "https://localhost:1234";
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: "https://localhost:1234"
|
||||||
|
});
|
||||||
getTokenFromAuthService("GET", "dbs", "foo");
|
getTokenFromAuthService("GET", "dbs", "foo");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
|
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
|
||||||
@@ -75,24 +80,28 @@ describe("getTokenFromAuthService", () => {
|
|||||||
|
|
||||||
describe("endpoint", () => {
|
describe("endpoint", () => {
|
||||||
it("falls back to _databaseAccount", () => {
|
it("falls back to _databaseAccount", () => {
|
||||||
CosmosClient.databaseAccount({
|
updateUserContext({
|
||||||
id: "foo",
|
databaseAccount: {
|
||||||
name: "foo",
|
id: "foo",
|
||||||
location: "foo",
|
name: "foo",
|
||||||
type: "foo",
|
location: "foo",
|
||||||
kind: "foo",
|
type: "foo",
|
||||||
tags: [],
|
kind: "foo",
|
||||||
properties: {
|
tags: [],
|
||||||
documentEndpoint: "bar",
|
properties: {
|
||||||
gremlinEndpoint: "foo",
|
documentEndpoint: "bar",
|
||||||
tableEndpoint: "foo",
|
gremlinEndpoint: "foo",
|
||||||
cassandraEndpoint: "foo"
|
tableEndpoint: "foo",
|
||||||
|
cassandraEndpoint: "foo"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(endpoint()).toEqual("bar");
|
expect(endpoint()).toEqual("bar");
|
||||||
});
|
});
|
||||||
it("uses _endpoint if set", () => {
|
it("uses _endpoint if set", () => {
|
||||||
CosmosClient.endpoint("baz");
|
updateUserContext({
|
||||||
|
endpoint: "baz"
|
||||||
|
});
|
||||||
expect(endpoint()).toEqual("baz");
|
expect(endpoint()).toEqual("baz");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -100,17 +109,17 @@ describe("endpoint", () => {
|
|||||||
describe("requestPlugin", () => {
|
describe("requestPlugin", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete window.dataExplorerPlatform;
|
delete window.dataExplorerPlatform;
|
||||||
delete config.PROXY_PATH;
|
resetConfigContext();
|
||||||
delete config.BACKEND_ENDPOINT;
|
|
||||||
delete config.PROXY_PATH;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Hosted", () => {
|
describe("Hosted", () => {
|
||||||
it("builds a proxy URL in development", () => {
|
it("builds a proxy URL in development", () => {
|
||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
config.platform = Platform.Hosted;
|
updateConfigContext({
|
||||||
config.BACKEND_ENDPOINT = "https://localhost:1234";
|
platform: Platform.Hosted,
|
||||||
config.PROXY_PATH = "/proxy";
|
BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
|
PROXY_PATH: "/proxy"
|
||||||
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
const endpoint = "https://docs.azure.com";
|
const endpoint = "https://docs.azure.com";
|
||||||
const path = "/dbs/foo";
|
const path = "/dbs/foo";
|
||||||
@@ -122,8 +131,7 @@ describe("requestPlugin", () => {
|
|||||||
describe("Emulator", () => {
|
describe("Emulator", () => {
|
||||||
it("builds a url for emulator proxy via webpack", () => {
|
it("builds a url for emulator proxy via webpack", () => {
|
||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
config.platform = Platform.Emulator;
|
updateConfigContext({ platform: Platform.Emulator, PROXY_PATH: "/proxy" });
|
||||||
config.PROXY_PATH = "/proxy";
|
|
||||||
const headers = {};
|
const headers = {};
|
||||||
const endpoint = "";
|
const endpoint = "";
|
||||||
const path = "/dbs/foo";
|
const path = "/dbs/foo";
|
||||||
|
|||||||
@@ -1,39 +1,28 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { configContext, Platform } from "../ConfigContext";
|
||||||
import { HttpHeaders, EmulatorMasterKey } from "./Constants";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { userContext } from "../UserContext";
|
||||||
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;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
if (config.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_masterKey) {
|
if (userContext.masterKey) {
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_resourceToken) {
|
if (userContext.resourceToken) {
|
||||||
return _resourceToken;
|
return userContext.resourceToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getTokenFromAuthService(verb, resourceType, resourceId);
|
const result = await getTokenFromAuthService(verb, resourceType, resourceId);
|
||||||
@@ -42,28 +31,33 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
||||||
requestContext.endpoint = config.PROXY_PATH;
|
requestContext.endpoint = configContext.PROXY_PATH;
|
||||||
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
||||||
return next(requestContext);
|
return next(requestContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const endpoint = () => {
|
export const endpoint = () => {
|
||||||
if (config.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
// In worker scope, _global(self).parent does not exist
|
// In worker scope, _global(self).parent does not exist
|
||||||
const location = _global.parent ? _global.parent.location : _global.location;
|
const location = _global.parent ? _global.parent.location : _global.location;
|
||||||
return config.EMULATOR_ENDPOINT || location.origin;
|
return configContext.EMULATOR_ENDPOINT || location.origin;
|
||||||
}
|
}
|
||||||
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
|
return (
|
||||||
|
userContext.endpoint ||
|
||||||
|
(userContext.databaseAccount &&
|
||||||
|
userContext.databaseAccount.properties &&
|
||||||
|
userContext.databaseAccount.properties.documentEndpoint)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
|
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const host = config.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint();
|
const host = configContext.BACKEND_ENDPOINT || _global.dataExplorer.extensionEndpoint();
|
||||||
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-ms-encrypted-auth-token": _accessToken
|
"x-ms-encrypted-auth-token": userContext.accessToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
verb,
|
verb,
|
||||||
@@ -75,106 +69,25 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
|||||||
const result = JSON.parse(await response.json());
|
const result = JSON.parse(await response.json());
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`);
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CosmosClient = {
|
export function client(): Cosmos.CosmosClient {
|
||||||
client(): Cosmos.CosmosClient {
|
const options: Cosmos.CosmosClientOptions = {
|
||||||
if (_client) {
|
endpoint: endpoint() || " ", // CosmosClient gets upset if we pass a falsy value here
|
||||||
return _client;
|
key: userContext.masterKey,
|
||||||
}
|
tokenProvider,
|
||||||
const options: Cosmos.CosmosClientOptions = {
|
connectionPolicy: {
|
||||||
endpoint: endpoint() || " ", // CosmosClient gets upset if we pass a falsy value here
|
enableEndpointDiscovery: false
|
||||||
key: _masterKey,
|
},
|
||||||
tokenProvider,
|
userAgentSuffix: "Azure Portal"
|
||||||
connectionPolicy: {
|
};
|
||||||
enableEndpointDiscovery: false
|
|
||||||
},
|
|
||||||
userAgentSuffix: "Azure Portal"
|
|
||||||
};
|
|
||||||
|
|
||||||
// In development we proxy requests to the backend via webpack. This is removed in production bundles.
|
// In development we proxy requests to the backend via webpack. This is removed in production bundles.
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
|
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
|
||||||
}
|
|
||||||
_client = new Cosmos.CosmosClient(options);
|
|
||||||
return _client;
|
|
||||||
},
|
|
||||||
|
|
||||||
authorizationToken(value?: string): string {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _authorizationToken;
|
|
||||||
}
|
|
||||||
_authorizationToken = value;
|
|
||||||
_client = null;
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
accessToken(value?: string): string {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _accessToken;
|
|
||||||
}
|
|
||||||
_accessToken = value;
|
|
||||||
_client = null;
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
masterKey(value?: string): string {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _masterKey;
|
|
||||||
}
|
|
||||||
_client = null;
|
|
||||||
_masterKey = value;
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
endpoint(value?: string): string {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _endpoint;
|
|
||||||
}
|
|
||||||
_client = null;
|
|
||||||
_endpoint = value;
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
databaseAccount(value?: DatabaseAccount): DatabaseAccount {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _databaseAccount || ({} as any);
|
|
||||||
}
|
|
||||||
_client = null;
|
|
||||||
_databaseAccount = value;
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
subscriptionId(value?: string): string {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _subscriptionId;
|
|
||||||
}
|
|
||||||
_client = null;
|
|
||||||
_subscriptionId = value;
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceGroup(value?: string): string {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _resourceGroup;
|
|
||||||
}
|
|
||||||
_client = null;
|
|
||||||
_resourceGroup = value;
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceToken(value?: string): string {
|
|
||||||
if (typeof value === "undefined") {
|
|
||||||
return _resourceToken;
|
|
||||||
}
|
|
||||||
_client = null;
|
|
||||||
_resourceToken = value;
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
};
|
return new Cosmos.CosmosClient(options);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,18 +6,16 @@ import * as ViewModels from "../Contracts/ViewModels";
|
|||||||
import Q from "q";
|
import Q from "q";
|
||||||
import {
|
import {
|
||||||
ConflictDefinition,
|
ConflictDefinition,
|
||||||
ContainerDefinition,
|
|
||||||
ContainerResponse,
|
|
||||||
DatabaseResponse,
|
|
||||||
FeedOptions,
|
FeedOptions,
|
||||||
ItemDefinition,
|
ItemDefinition,
|
||||||
PartitionKeyDefinition,
|
PartitionKeyDefinition,
|
||||||
QueryIterator,
|
QueryIterator,
|
||||||
Resource,
|
Resource,
|
||||||
TriggerDefinition
|
TriggerDefinition,
|
||||||
|
OfferDefinition
|
||||||
} from "@azure/cosmos";
|
} from "@azure/cosmos";
|
||||||
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
||||||
import { CosmosClient } from "./CosmosClient";
|
import { client } from "./CosmosClient";
|
||||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||||
import { sendCachedDataMessage } from "./MessageHandler";
|
import { sendCachedDataMessage } from "./MessageHandler";
|
||||||
@@ -25,8 +23,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
|
|||||||
import { OfferUtils } from "../Utils/OfferUtils";
|
import { OfferUtils } from "../Utils/OfferUtils";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||||
import { Platform, config } from "../Config";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||||
|
|
||||||
@@ -54,7 +51,7 @@ export function queryDocuments(
|
|||||||
options: any
|
options: any
|
||||||
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
|
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
|
||||||
options = getCommonQueryOptions(options);
|
options = getCommonQueryOptions(options);
|
||||||
const documentsIterator = CosmosClient.client()
|
const documentsIterator = client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
.container(containerId)
|
.container(containerId)
|
||||||
.items.query(query, options);
|
.items.query(query, options);
|
||||||
@@ -66,7 +63,7 @@ export function readStoredProcedures(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.StoredProcedure[]> {
|
): Q.Promise<DataModels.StoredProcedure[]> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.storedProcedures.readAll(options)
|
.scripts.storedProcedures.readAll(options)
|
||||||
@@ -81,7 +78,7 @@ export function readStoredProcedure(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.StoredProcedure> {
|
): Q.Promise<DataModels.StoredProcedure> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.storedProcedure(requestedResource.id)
|
.scripts.storedProcedure(requestedResource.id)
|
||||||
@@ -94,7 +91,7 @@ export function readUserDefinedFunctions(
|
|||||||
options: any
|
options: any
|
||||||
): Q.Promise<DataModels.UserDefinedFunction[]> {
|
): Q.Promise<DataModels.UserDefinedFunction[]> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.userDefinedFunctions.readAll(options)
|
.scripts.userDefinedFunctions.readAll(options)
|
||||||
@@ -108,7 +105,7 @@ export function readUserDefinedFunction(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.userDefinedFunction(requestedResource.id)
|
.scripts.userDefinedFunction(requestedResource.id)
|
||||||
@@ -119,7 +116,7 @@ export function readUserDefinedFunction(
|
|||||||
|
|
||||||
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
|
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.triggers.readAll(options)
|
.scripts.triggers.readAll(options)
|
||||||
@@ -134,7 +131,7 @@ export function readTrigger(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.Trigger> {
|
): Q.Promise<DataModels.Trigger> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.trigger(requestedResource.id)
|
.scripts.trigger(requestedResource.id)
|
||||||
@@ -152,7 +149,7 @@ export function executeStoredProcedure(
|
|||||||
// TODO remove this deferred. Kept it because of timeout code at bottom of function
|
// TODO remove this deferred. Kept it because of timeout code at bottom of function
|
||||||
const deferred = Q.defer<any>();
|
const deferred = Q.defer<any>();
|
||||||
|
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.storedProcedure(storedProcedure.id())
|
.scripts.storedProcedure(storedProcedure.id())
|
||||||
@@ -175,7 +172,7 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
|
|||||||
const partitionKey = documentId.partitionKeyValue;
|
const partitionKey = documentId.partitionKeyValue;
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.item(documentId.id(), partitionKey)
|
.item(documentId.id(), partitionKey)
|
||||||
@@ -203,23 +200,6 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
|
|||||||
return [partitionKeyValue];
|
return [partitionKeyValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function 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 refreshCachedResources().then(() => response.resource as DataModels.Collection);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateDocument(
|
export function updateDocument(
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
@@ -228,7 +208,7 @@ export function updateDocument(
|
|||||||
const partitionKey = documentId.partitionKeyValue;
|
const partitionKey = documentId.partitionKeyValue;
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.item(documentId.id(), partitionKey)
|
.item(documentId.id(), partitionKey)
|
||||||
@@ -243,9 +223,10 @@ export function updateOffer(
|
|||||||
options?: RequestOptions
|
options?: RequestOptions
|
||||||
): Q.Promise<DataModels.Offer> {
|
): Q.Promise<DataModels.Offer> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.offer(offer.id)
|
.offer(offer.id)
|
||||||
.replace(newOffer, options)
|
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||||
|
.replace((newOffer as unknown) as OfferDefinition, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
||||||
})
|
})
|
||||||
@@ -258,7 +239,7 @@ export function updateStoredProcedure(
|
|||||||
options: any
|
options: any
|
||||||
): Q.Promise<DataModels.StoredProcedure> {
|
): Q.Promise<DataModels.StoredProcedure> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.storedProcedure(storedProcedure.id)
|
.scripts.storedProcedure(storedProcedure.id)
|
||||||
@@ -273,7 +254,7 @@ export function updateUserDefinedFunction(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.userDefinedFunction(userDefinedFunction.id)
|
.scripts.userDefinedFunction(userDefinedFunction.id)
|
||||||
@@ -288,7 +269,7 @@ export function updateTrigger(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.Trigger> {
|
): Q.Promise<DataModels.Trigger> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.trigger(trigger.id)
|
.scripts.trigger(trigger.id)
|
||||||
@@ -299,7 +280,7 @@ export function updateTrigger(
|
|||||||
|
|
||||||
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
|
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.items.create(newDocument)
|
.items.create(newDocument)
|
||||||
@@ -313,7 +294,7 @@ export function createStoredProcedure(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.StoredProcedure> {
|
): Q.Promise<DataModels.StoredProcedure> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.storedProcedures.create(newStoredProcedure, options)
|
.scripts.storedProcedures.create(newStoredProcedure, options)
|
||||||
@@ -327,7 +308,7 @@ export function createUserDefinedFunction(
|
|||||||
options: any
|
options: any
|
||||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.userDefinedFunctions.create(newUserDefinedFunction, options)
|
.scripts.userDefinedFunctions.create(newUserDefinedFunction, options)
|
||||||
@@ -341,7 +322,7 @@ export function createTrigger(
|
|||||||
options?: any
|
options?: any
|
||||||
): Q.Promise<DataModels.Trigger> {
|
): Q.Promise<DataModels.Trigger> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.triggers.create(newTrigger as TriggerDefinition, options)
|
.scripts.triggers.create(newTrigger as TriggerDefinition, options)
|
||||||
@@ -353,7 +334,7 @@ export function deleteDocument(collection: ViewModels.CollectionBase, documentId
|
|||||||
const partitionKey = documentId.partitionKeyValue;
|
const partitionKey = documentId.partitionKeyValue;
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.item(documentId.id(), partitionKey)
|
.item(documentId.id(), partitionKey)
|
||||||
@@ -369,7 +350,7 @@ export function deleteConflict(
|
|||||||
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
|
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.conflict(conflictId.id())
|
.conflict(conflictId.id())
|
||||||
@@ -383,7 +364,7 @@ export function deleteStoredProcedure(
|
|||||||
options: any
|
options: any
|
||||||
): Q.Promise<any> {
|
): Q.Promise<any> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.storedProcedure(storedProcedure.id)
|
.scripts.storedProcedure(storedProcedure.id)
|
||||||
@@ -397,7 +378,7 @@ export function deleteUserDefinedFunction(
|
|||||||
options: any
|
options: any
|
||||||
): Q.Promise<any> {
|
): Q.Promise<any> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.userDefinedFunction(userDefinedFunction.id)
|
.scripts.userDefinedFunction(userDefinedFunction.id)
|
||||||
@@ -411,7 +392,7 @@ export function deleteTrigger(
|
|||||||
options: any
|
options: any
|
||||||
): Q.Promise<any> {
|
): Q.Promise<any> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.scripts.trigger(trigger.id)
|
.scripts.trigger(trigger.id)
|
||||||
@@ -419,26 +400,6 @@ export function deleteTrigger(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function 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[])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function 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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readCollectionQuotaInfo(
|
export function readCollectionQuotaInfo(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
options: any
|
options: any
|
||||||
@@ -449,7 +410,7 @@ export function readCollectionQuotaInfo(
|
|||||||
options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true;
|
options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true;
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.read(options)
|
.read(options)
|
||||||
@@ -475,8 +436,12 @@ export function readCollectionQuotaInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
|
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
|
||||||
|
if (options.isServerless) {
|
||||||
|
return Q([]); // Reading offers is not supported for serverless accounts
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (config.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
|
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
|
||||||
(<any>window).dataExplorer.databaseAccount().id,
|
(<any>window).dataExplorer.databaseAccount().id,
|
||||||
Constants.ClientDefaults.portalCacheTimeoutMs
|
Constants.ClientDefaults.portalCacheTimeoutMs
|
||||||
@@ -486,10 +451,17 @@ export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
|
|||||||
// If error getting cached Offers, continue on and read via SDK
|
// If error getting cached Offers, continue on and read via SDK
|
||||||
}
|
}
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.offers.readAll()
|
.offers.readAll()
|
||||||
.fetchAll()
|
.fetchAll()
|
||||||
.then(response => response.resources)
|
.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;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,33 +473,13 @@ export function readOffer(requestedResource: DataModels.Offer, options: any): Q.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.offer(requestedResource.id)
|
.offer(requestedResource.id)
|
||||||
.read(options)
|
.read(options)
|
||||||
.then(response => ({ ...response.resource, headers: response.headers }))
|
.then(response => ({ ...response.resource, headers: response.headers }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDatabases(options: any): Q.Promise<DataModels.Database[]> {
|
|
||||||
try {
|
|
||||||
if (config.platform === Platform.Portal) {
|
|
||||||
return sendCachedDataMessage<DataModels.Database[]>(MessageTypes.AllDatabases, [
|
|
||||||
(<any>window).dataExplorer.databaseAccount().id,
|
|
||||||
Constants.ClientDefaults.portalCacheTimeoutMs
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If error getting cached DBs, continue on and read via SDK
|
|
||||||
}
|
|
||||||
|
|
||||||
return Q(
|
|
||||||
CosmosClient.client()
|
|
||||||
.databases.readAll()
|
|
||||||
.fetchAll()
|
|
||||||
.then(response => response.resources as DataModels.Database[])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOrCreateDatabaseAndCollection(
|
export function getOrCreateDatabaseAndCollection(
|
||||||
request: DataModels.CreateDatabaseAndCollectionRequest,
|
request: DataModels.CreateDatabaseAndCollectionRequest,
|
||||||
options: any
|
options: any
|
||||||
@@ -569,7 +521,7 @@ export function getOrCreateDatabaseAndCollection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.databases.createIfNotExists(createBody, databaseOptions)
|
.databases.createIfNotExists(createBody, databaseOptions)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.database.containers.create(
|
return response.database.containers.create(
|
||||||
@@ -591,28 +543,8 @@ export function getOrCreateDatabaseAndCollection(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDatabase(
|
|
||||||
request: DataModels.CreateDatabaseRequest,
|
|
||||||
options: any
|
|
||||||
): Q.Promise<DataModels.Database> {
|
|
||||||
var deferred = Q.defer<DataModels.Database>();
|
|
||||||
|
|
||||||
_createDatabase(request, options).then(
|
|
||||||
(createdDatabase: DataModels.Database) => {
|
|
||||||
refreshCachedOffers().then(() => {
|
|
||||||
deferred.resolve(createdDatabase);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
_createDatabaseError => {
|
|
||||||
deferred.reject(_createDatabaseError);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function refreshCachedOffers(): Q.Promise<void> {
|
export function refreshCachedOffers(): Q.Promise<void> {
|
||||||
if (config.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
|
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
|
||||||
} else {
|
} else {
|
||||||
return Q();
|
return Q();
|
||||||
@@ -620,7 +552,7 @@ export function refreshCachedOffers(): Q.Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function refreshCachedResources(options?: any): Q.Promise<void> {
|
export function refreshCachedResources(options?: any): Q.Promise<void> {
|
||||||
if (config.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
return sendCachedDataMessage(MessageTypes.RefreshResources, []);
|
return sendCachedDataMessage(MessageTypes.RefreshResources, []);
|
||||||
} else {
|
} else {
|
||||||
return Q();
|
return Q();
|
||||||
@@ -633,62 +565,9 @@ export function queryConflicts(
|
|||||||
query: string,
|
query: string,
|
||||||
options: any
|
options: any
|
||||||
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
|
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
|
||||||
const documentsIterator = CosmosClient.client()
|
const documentsIterator = client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
.container(containerId)
|
.container(containerId)
|
||||||
.conflicts.query(query, options);
|
.conflicts.query(query, options);
|
||||||
return Q(documentsIterator);
|
return Q(documentsIterator);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateOfferThroughputBeyondLimit(
|
|
||||||
request: DataModels.UpdateOfferThroughputRequest
|
|
||||||
): Promise<void> {
|
|
||||||
if (config.platform !== Platform.Portal) {
|
|
||||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
throw new Error(await response.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
function _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 refreshCachedResources(databaseOptions).then(() => response.resource);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -266,42 +266,6 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function 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()}`
|
|
||||||
);
|
|
||||||
DataAccessUtilityBase.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);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateDocument(
|
export function updateDocument(
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
@@ -383,40 +347,6 @@ export function updateOffer(
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateOfferThroughputBeyondLimit(
|
|
||||||
requestPayload: DataModels.UpdateOfferThroughputRequest
|
|
||||||
): 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}`
|
|
||||||
);
|
|
||||||
DataAccessUtilityBase.updateOfferThroughputBeyondLimit(requestPayload)
|
|
||||||
.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)}`
|
|
||||||
);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
|
||||||
|
|
||||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateStoredProcedure(
|
export function updateStoredProcedure(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
storedProcedure: DataModels.StoredProcedure,
|
storedProcedure: DataModels.StoredProcedure,
|
||||||
@@ -840,63 +770,6 @@ export function refreshCachedOffers(): Q.Promise<void> {
|
|||||||
return DataAccessUtilityBase.refreshCachedOffers();
|
return DataAccessUtilityBase.refreshCachedOffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function 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()}`
|
|
||||||
);
|
|
||||||
DataAccessUtilityBase.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);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readCollection(databaseId: string, collectionId: string): Q.Promise<DataModels.Collection> {
|
|
||||||
const deferred = Q.defer<DataModels.Collection>();
|
|
||||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.InProgress,
|
|
||||||
`Querying container ${collectionId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
DataAccessUtilityBase.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);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readCollectionQuotaInfo(
|
export function readCollectionQuotaInfo(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
options?: any
|
options?: any
|
||||||
@@ -984,31 +857,6 @@ export function readOffer(
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDatabases(options: any): Q.Promise<DataModels.Database[]> {
|
|
||||||
var deferred = Q.defer<any>();
|
|
||||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying databases");
|
|
||||||
DataAccessUtilityBase.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);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOrCreateDatabaseAndCollection(
|
export function getOrCreateDatabaseAndCollection(
|
||||||
request: DataModels.CreateDatabaseAndCollectionRequest,
|
request: DataModels.CreateDatabaseAndCollectionRequest,
|
||||||
options: any = {}
|
options: any = {}
|
||||||
@@ -1042,36 +890,3 @@ export function getOrCreateDatabaseAndCollection(
|
|||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function 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}`
|
|
||||||
);
|
|
||||||
|
|
||||||
DataAccessUtilityBase.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)}`
|
|
||||||
);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
export function replaceKnownError(err: string): string {
|
export function replaceKnownError(err: string): string {
|
||||||
if (
|
if (
|
||||||
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
|
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
|
||||||
err.indexOf("SharedOffer is Disabled for your account") >= 0
|
err.indexOf("SharedOffer is Disabled for your account") >= 0
|
||||||
) {
|
) {
|
||||||
return "Database throughput is not supported for internal subscriptions.";
|
return "Database throughput is not supported for internal subscriptions.";
|
||||||
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
|
} 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 "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parse(err: any): DataModels.ErrorDataModel[] {
|
export function parse(err: any): DataModels.ErrorDataModel[] {
|
||||||
try {
|
try {
|
||||||
return _parse(err);
|
return _parse(err);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
|
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _parse(err: any): DataModels.ErrorDataModel[] {
|
function _parse(err: any): DataModels.ErrorDataModel[] {
|
||||||
var normalizedErrors: DataModels.ErrorDataModel[] = [];
|
var normalizedErrors: DataModels.ErrorDataModel[] = [];
|
||||||
if (err.message && !err.code) {
|
if (err.message && !err.code) {
|
||||||
normalizedErrors.push(err);
|
normalizedErrors.push(err);
|
||||||
} else {
|
} else {
|
||||||
const innerErrors: any[] = _getInnerErrors(err.message);
|
const innerErrors: any[] = _getInnerErrors(err.message);
|
||||||
normalizedErrors = innerErrors.map(innerError =>
|
normalizedErrors = innerErrors.map(innerError =>
|
||||||
typeof innerError === "string" ? { message: innerError } : innerError
|
typeof innerError === "string" ? { message: innerError } : innerError
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizedErrors;
|
return normalizedErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getInnerErrors(message: string): any[] {
|
function _getInnerErrors(message: string): any[] {
|
||||||
/*
|
/*
|
||||||
The backend error message has an inner-message which is a stringified object.
|
The backend error message has an inner-message which is a stringified object.
|
||||||
|
|
||||||
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
||||||
Example:
|
Example:
|
||||||
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
||||||
For non-SQL errors the "Errors" propery is an array of string.
|
For non-SQL errors the "Errors" propery is an array of string.
|
||||||
Example:
|
Example:
|
||||||
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let innerMessage: any = null;
|
let innerMessage: any = null;
|
||||||
|
|
||||||
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
||||||
try {
|
try {
|
||||||
// Multi-Partition error flavor
|
// Multi-Partition error flavor
|
||||||
const regExp = /^(.*)ActivityId: (.*)/g;
|
const regExp = /^(.*)ActivityId: (.*)/g;
|
||||||
const regString = regExp.exec(singleLineMessage);
|
const regString = regExp.exec(singleLineMessage);
|
||||||
const innerMessageString = regString[1];
|
const innerMessageString = regString[1];
|
||||||
innerMessage = JSON.parse(innerMessageString);
|
innerMessage = JSON.parse(innerMessageString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Single-partition error flavor
|
// Single-partition error flavor
|
||||||
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
||||||
const regString = regExp.exec(singleLineMessage);
|
const regString = regExp.exec(singleLineMessage);
|
||||||
const innerMessageString = regString[1];
|
const innerMessageString = regString[1];
|
||||||
innerMessage = JSON.parse(innerMessageString);
|
innerMessage = JSON.parse(innerMessageString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,73 @@
|
|||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "./Constants";
|
import * as Constants from "./Constants";
|
||||||
|
|
||||||
export interface CachedDataPromise<T> {
|
export interface CachedDataPromise<T> {
|
||||||
deferred: Q.Deferred<T>;
|
deferred: Q.Deferred<T>;
|
||||||
startTime: Date;
|
startTime: Date;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RequestMap: Record<string, CachedDataPromise<any>> = {};
|
export const RequestMap: Record<string, CachedDataPromise<any>> = {};
|
||||||
|
|
||||||
export function handleCachedDataMessage(message: any): void {
|
export function handleCachedDataMessage(message: any): void {
|
||||||
const messageContent = message && message.message;
|
const messageContent = message && message.message;
|
||||||
if (message == null || messageContent == null || messageContent.id == null || !RequestMap[messageContent.id]) {
|
if (message == null || messageContent == null || messageContent.id == null || !RequestMap[messageContent.id]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedDataPromise = RequestMap[messageContent.id];
|
const cachedDataPromise = RequestMap[messageContent.id];
|
||||||
if (messageContent.error != null) {
|
if (messageContent.error != null) {
|
||||||
cachedDataPromise.deferred.reject(messageContent.error);
|
cachedDataPromise.deferred.reject(messageContent.error);
|
||||||
} else {
|
} else {
|
||||||
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
|
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
|
||||||
}
|
}
|
||||||
runGarbageCollector();
|
runGarbageCollector();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendCachedDataMessage<TResponseDataModel>(
|
export function sendCachedDataMessage<TResponseDataModel>(
|
||||||
messageType: MessageTypes,
|
messageType: MessageTypes,
|
||||||
params: Object[],
|
params: Object[],
|
||||||
timeoutInMs?: number
|
timeoutInMs?: number
|
||||||
): Q.Promise<TResponseDataModel> {
|
): Q.Promise<TResponseDataModel> {
|
||||||
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
||||||
deferred: Q.defer<TResponseDataModel>(),
|
deferred: Q.defer<TResponseDataModel>(),
|
||||||
startTime: new Date(),
|
startTime: new Date(),
|
||||||
id: _.uniqueId()
|
id: _.uniqueId()
|
||||||
};
|
};
|
||||||
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
||||||
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||||
|
|
||||||
//TODO: Use telemetry to measure optimal time to resolve/reject promises
|
//TODO: Use telemetry to measure optimal time to resolve/reject promises
|
||||||
return cachedDataPromise.deferred.promise.timeout(
|
return cachedDataPromise.deferred.promise.timeout(
|
||||||
timeoutInMs || Constants.ClientDefaults.requestTimeoutMs,
|
timeoutInMs || Constants.ClientDefaults.requestTimeoutMs,
|
||||||
"Timed out while waiting for response from portal"
|
"Timed out while waiting for response from portal"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMessage(data: any): void {
|
export function sendMessage(data: any): void {
|
||||||
if (canSendMessage()) {
|
if (canSendMessage()) {
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{
|
{
|
||||||
signature: "pcIframe",
|
signature: "pcIframe",
|
||||||
data: data
|
data: data
|
||||||
},
|
},
|
||||||
window.document.referrer
|
window.document.referrer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canSendMessage(): boolean {
|
export function canSendMessage(): boolean {
|
||||||
return window.parent !== window;
|
return window.parent !== window;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is exported just for testing. It should not be.
|
// TODO: This is exported just for testing. It should not be.
|
||||||
export function runGarbageCollector() {
|
export function runGarbageCollector() {
|
||||||
Object.keys(RequestMap).forEach((key: string) => {
|
Object.keys(RequestMap).forEach((key: string) => {
|
||||||
const promise: Q.Promise<any> = RequestMap[key].deferred.promise;
|
const promise: Q.Promise<any> = RequestMap[key].deferred.promise;
|
||||||
if (promise.isFulfilled() || promise.isRejected()) {
|
if (promise.isFulfilled() || promise.isRejected()) {
|
||||||
delete RequestMap[key];
|
delete RequestMap[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
|
import { AuthType } from "../AuthType";
|
||||||
|
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
||||||
|
import { updateUserContext } from "../UserContext";
|
||||||
import {
|
import {
|
||||||
_createMongoCollectionWithARM,
|
|
||||||
deleteDocument,
|
deleteDocument,
|
||||||
getEndpoint,
|
getEndpoint,
|
||||||
queryDocuments,
|
queryDocuments,
|
||||||
readDocument,
|
readDocument,
|
||||||
updateDocument
|
updateDocument,
|
||||||
|
_createMongoCollectionWithARM
|
||||||
} from "./MongoProxyClient";
|
} from "./MongoProxyClient";
|
||||||
import { AuthType } from "../AuthType";
|
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
|
||||||
import { config } from "../Config";
|
|
||||||
import { CosmosClient } from "./CosmosClient";
|
|
||||||
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
|
||||||
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
@@ -62,13 +62,15 @@ const databaseAccount = {
|
|||||||
tableEndpoint: "foo",
|
tableEndpoint: "foo",
|
||||||
cassandraEndpoint: "foo"
|
cassandraEndpoint: "foo"
|
||||||
}
|
}
|
||||||
};
|
} as DatabaseAccount;
|
||||||
|
|
||||||
describe("MongoProxyClient", () => {
|
describe("MongoProxyClient", () => {
|
||||||
describe("queryDocuments", () => {
|
describe("queryDocuments", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete config.BACKEND_ENDPOINT;
|
resetConfigContext();
|
||||||
CosmosClient.databaseAccount(databaseAccount as any);
|
updateUserContext({
|
||||||
|
databaseAccount
|
||||||
|
});
|
||||||
window.dataExplorer = {
|
window.dataExplorer = {
|
||||||
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
||||||
serverId: () => ""
|
serverId: () => ""
|
||||||
@@ -88,7 +90,7 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
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",
|
"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",
|
||||||
@@ -98,8 +100,10 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
describe("readDocument", () => {
|
describe("readDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete config.MONGO_BACKEND_ENDPOINT;
|
resetConfigContext();
|
||||||
CosmosClient.databaseAccount(databaseAccount as any);
|
updateUserContext({
|
||||||
|
databaseAccount
|
||||||
|
});
|
||||||
window.dataExplorer = {
|
window.dataExplorer = {
|
||||||
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
||||||
serverId: () => ""
|
serverId: () => ""
|
||||||
@@ -119,7 +123,7 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
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",
|
"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",
|
||||||
@@ -129,8 +133,10 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
describe("createDocument", () => {
|
describe("createDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete config.MONGO_BACKEND_ENDPOINT;
|
resetConfigContext();
|
||||||
CosmosClient.databaseAccount(databaseAccount as any);
|
updateUserContext({
|
||||||
|
databaseAccount
|
||||||
|
});
|
||||||
window.dataExplorer = {
|
window.dataExplorer = {
|
||||||
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
||||||
serverId: () => ""
|
serverId: () => ""
|
||||||
@@ -150,7 +156,7 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
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",
|
"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",
|
||||||
@@ -160,8 +166,10 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
describe("updateDocument", () => {
|
describe("updateDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete config.MONGO_BACKEND_ENDPOINT;
|
resetConfigContext();
|
||||||
CosmosClient.databaseAccount(databaseAccount as any);
|
updateUserContext({
|
||||||
|
databaseAccount
|
||||||
|
});
|
||||||
window.dataExplorer = {
|
window.dataExplorer = {
|
||||||
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
||||||
serverId: () => ""
|
serverId: () => ""
|
||||||
@@ -181,7 +189,7 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
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",
|
"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",
|
||||||
@@ -191,8 +199,10 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
describe("deleteDocument", () => {
|
describe("deleteDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete config.MONGO_BACKEND_ENDPOINT;
|
resetConfigContext();
|
||||||
CosmosClient.databaseAccount(databaseAccount as any);
|
updateUserContext({
|
||||||
|
databaseAccount
|
||||||
|
});
|
||||||
window.dataExplorer = {
|
window.dataExplorer = {
|
||||||
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
||||||
serverId: () => ""
|
serverId: () => ""
|
||||||
@@ -212,7 +222,7 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
deleteDocument(databaseId, collection, documentId);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
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",
|
"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",
|
||||||
@@ -222,9 +232,11 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
describe("getEndpoint", () => {
|
describe("getEndpoint", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete config.MONGO_BACKEND_ENDPOINT;
|
resetConfigContext();
|
||||||
delete window.authType;
|
delete window.authType;
|
||||||
CosmosClient.databaseAccount(databaseAccount as any);
|
updateUserContext({
|
||||||
|
databaseAccount
|
||||||
|
});
|
||||||
window.dataExplorer = {
|
window.dataExplorer = {
|
||||||
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
extensionEndpoint: () => "https://main.documentdb.ext.azure.com",
|
||||||
serverId: () => ""
|
serverId: () => ""
|
||||||
@@ -237,7 +249,7 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns a development endpoint", () => {
|
it("returns a development endpoint", () => {
|
||||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:1234";
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
|
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
|
||||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +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 Constants from "../Common/Constants";
|
||||||
import * as DataExplorerConstants from "../Common/Constants";
|
import * as DataExplorerConstants from "../Common/Constants";
|
||||||
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
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 { sendMessage } from "./MessageHandler";
|
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
||||||
|
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
|
||||||
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
|
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
|
import { userContext } from "../UserContext";
|
||||||
|
import EnvironmentUtility from "./EnvironmentUtility";
|
||||||
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
[HttpHeaders.apiType]: ApiType.MongoDB.toString(),
|
[HttpHeaders.apiType]: ApiType.MongoDB.toString(),
|
||||||
@@ -26,9 +26,9 @@ const defaultHeaders = {
|
|||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
if (window.authType === AuthType.EncryptedToken) {
|
if (window.authType === AuthType.EncryptedToken) {
|
||||||
return { [HttpHeaders.guestAccessToken]: CosmosClient.accessToken() };
|
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
|
||||||
} else {
|
} else {
|
||||||
return { [HttpHeaders.authorization]: CosmosClient.authorizationToken() };
|
return { [HttpHeaders.authorization]: userContext.authorizationToken };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
|||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string
|
continuationToken?: string
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
db: databaseId,
|
db: databaseId,
|
||||||
@@ -75,8 +75,8 @@ export function queryDocuments(
|
|||||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||||
rid: collection.rid,
|
rid: collection.rid,
|
||||||
rtype: "docs",
|
rtype: "docs",
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: databaseAccount.name,
|
dba: databaseAccount.name,
|
||||||
pk:
|
pk:
|
||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
|
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
|
||||||
@@ -125,7 +125,7 @@ export function readDocument(
|
|||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId
|
documentId: DocumentId
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
const path = idComponents.slice(0, 4).join("/");
|
const path = idComponents.slice(0, 4).join("/");
|
||||||
@@ -136,8 +136,8 @@ export function readDocument(
|
|||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
rid,
|
rid,
|
||||||
rtype: "docs",
|
rtype: "docs",
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: databaseAccount.name,
|
dba: databaseAccount.name,
|
||||||
pk:
|
pk:
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||||
@@ -169,7 +169,7 @@ export function createDocument(
|
|||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown
|
documentContent: unknown
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
db: databaseId,
|
db: databaseId,
|
||||||
@@ -177,8 +177,8 @@ export function createDocument(
|
|||||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||||
rid: collection.rid,
|
rid: collection.rid,
|
||||||
rtype: "docs",
|
rtype: "docs",
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: databaseAccount.name,
|
dba: databaseAccount.name,
|
||||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
|
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
|
||||||
};
|
};
|
||||||
@@ -208,7 +208,7 @@ export function updateDocument(
|
|||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string
|
documentContent: string
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
const path = idComponents.slice(0, 5).join("/");
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
@@ -219,8 +219,8 @@ export function updateDocument(
|
|||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
rid,
|
rid,
|
||||||
rtype: "docs",
|
rtype: "docs",
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: databaseAccount.name,
|
dba: databaseAccount.name,
|
||||||
pk:
|
pk:
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||||
@@ -247,7 +247,7 @@ export function updateDocument(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
const path = idComponents.slice(0, 5).join("/");
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
@@ -258,8 +258,8 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
|||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
rid,
|
rid,
|
||||||
rtype: "docs",
|
rtype: "docs",
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: databaseAccount.name,
|
dba: databaseAccount.name,
|
||||||
pk:
|
pk:
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||||
@@ -294,7 +294,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: boolean,
|
isSharded: boolean,
|
||||||
autopilotOptions?: DataModels.RpOptions
|
autopilotOptions?: DataModels.RpOptions
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const params: DataModels.MongoParameters = {
|
const params: DataModels.MongoParameters = {
|
||||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||||
db: databaseId,
|
db: databaseId,
|
||||||
@@ -306,8 +306,8 @@ export function createMongoCollectionWithProxy(
|
|||||||
is: isSharded,
|
is: isSharded,
|
||||||
rid: "",
|
rid: "",
|
||||||
rtype: "colls",
|
rtype: "colls",
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: databaseAccount.name,
|
dba: databaseAccount.name,
|
||||||
isAutoPilot: false
|
isAutoPilot: false
|
||||||
};
|
};
|
||||||
@@ -351,7 +351,7 @@ export function createMongoCollectionWithARM(
|
|||||||
isSharded: boolean,
|
isSharded: boolean,
|
||||||
additionalOptions?: DataModels.RpOptions
|
additionalOptions?: DataModels.RpOptions
|
||||||
): Promise<DataModels.CreateCollectionWithRpResponse> {
|
): Promise<DataModels.CreateCollectionWithRpResponse> {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const params: DataModels.MongoParameters = {
|
const params: DataModels.MongoParameters = {
|
||||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||||
db: databaseId,
|
db: databaseId,
|
||||||
@@ -363,8 +363,8 @@ export function createMongoCollectionWithARM(
|
|||||||
is: isSharded,
|
is: isSharded,
|
||||||
rid: "",
|
rid: "",
|
||||||
rtype: "colls",
|
rtype: "colls",
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: databaseAccount.name,
|
dba: databaseAccount.name,
|
||||||
analyticalStorageTtl
|
analyticalStorageTtl
|
||||||
};
|
};
|
||||||
@@ -384,8 +384,8 @@ export function createMongoCollectionWithARM(
|
|||||||
export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
|
export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
|
||||||
const serverId = window.dataExplorer.serverId();
|
const serverId = window.dataExplorer.serverId();
|
||||||
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
|
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
|
||||||
let url = config.MONGO_BACKEND_ENDPOINT
|
let url = configContext.MONGO_BACKEND_ENDPOINT
|
||||||
? config.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
|
? configContext.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
|
||||||
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
|
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
|
||||||
|
|
||||||
if (window.authType === AuthType.EncryptedToken) {
|
if (window.authType === AuthType.EncryptedToken) {
|
||||||
@@ -411,9 +411,7 @@ async function errorHandling(response: Response, action: string, params: unknown
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
|
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
|
||||||
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${
|
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
|
||||||
CosmosClient.databaseAccount().name
|
|
||||||
}/mongodbDatabases/${params.db}/collections/${params.coll}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function _createMongoCollectionWithARM(
|
export async function _createMongoCollectionWithARM(
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import "jquery";
|
|||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
import { CosmosClient } from "./CosmosClient";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export class NotificationsClientBase {
|
export class NotificationsClientBase {
|
||||||
private _extensionEndpoint: string;
|
private _extensionEndpoint: string;
|
||||||
@@ -16,10 +15,10 @@ export class NotificationsClientBase {
|
|||||||
|
|
||||||
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
|
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
|
||||||
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
|
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const subscriptionId: string = CosmosClient.subscriptionId();
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup: string = CosmosClient.resourceGroup();
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const url: string = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
const url = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
||||||
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
const headers: any = {};
|
const headers: any = {};
|
||||||
headers[authorizationHeader.header] = authorizationHeader.token;
|
headers[authorizationHeader.header] = authorizationHeader.token;
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
|
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import * as ErrorParserUtility from "./ErrorParserUtility";
|
|
||||||
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
|
||||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { CosmosClient } from "./CosmosClient";
|
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||||
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import * as Logger from "./Logger";
|
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { QueryUtils } from "../Utils/QueryUtils";
|
import { QueryUtils } from "../Utils/QueryUtils";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||||
|
import { userContext } from "../UserContext";
|
||||||
import {
|
import {
|
||||||
getOrCreateDatabaseAndCollection,
|
|
||||||
createDocument,
|
createDocument,
|
||||||
|
deleteDocument,
|
||||||
|
getOrCreateDatabaseAndCollection,
|
||||||
queryDocuments,
|
queryDocuments,
|
||||||
queryDocumentsPage,
|
queryDocumentsPage
|
||||||
deleteDocument
|
|
||||||
} from "./DocumentClientUtilityBase";
|
} from "./DocumentClientUtilityBase";
|
||||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
import * as ErrorParserUtility from "./ErrorParserUtility";
|
||||||
|
import * as Logger from "./Logger";
|
||||||
|
|
||||||
export class QueriesClient {
|
export class QueriesClient {
|
||||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||||
@@ -249,10 +249,10 @@ export class QueriesClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getResourceId(): string {
|
public getResourceId(): string {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const databaseAccountName: string = (databaseAccount && databaseAccount.name) || "";
|
const databaseAccountName = (databaseAccount && databaseAccount.name) || "";
|
||||||
const subscriptionId: string = CosmosClient.subscriptionId() || "";
|
const subscriptionId = userContext.subscriptionId || "";
|
||||||
const resourceGroup: string = CosmosClient.resourceGroup() || "";
|
const resourceGroup = userContext.resourceGroup || "";
|
||||||
|
|
||||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`;
|
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`;
|
||||||
}
|
}
|
||||||
|
|||||||
250
src/Common/dataAccess/createDatabase.ts
Normal file
250
src/Common/dataAccess/createDatabase.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseResponse } from "@azure/cosmos";
|
||||||
|
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import {
|
||||||
|
SqlDatabaseCreateUpdateParameters,
|
||||||
|
CreateUpdateOptions
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import {
|
||||||
|
createUpdateCassandraKeyspace,
|
||||||
|
getCassandraKeyspace
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import {
|
||||||
|
createUpdateMongoDBDatabase,
|
||||||
|
getMongoDBDatabase
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import {
|
||||||
|
createUpdateGremlinDatabase,
|
||||||
|
getGremlinDatabase
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
let database: DataModels.Database;
|
||||||
|
const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
!userContext.useSDKOperations &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
|
) {
|
||||||
|
database = await createDatabaseWithARM(params);
|
||||||
|
} else {
|
||||||
|
database = await createDatabaseWithSDK(params);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while creating database ${params.databaseId}:\n ${error.message}`);
|
||||||
|
logError(JSON.stringify(error), "CreateDatabase", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
clearMessage();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logConsoleInfo(`Successfully created database ${params.databaseId}`);
|
||||||
|
await refreshCachedResources();
|
||||||
|
await refreshCachedOffers();
|
||||||
|
clearMessage();
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
return createSqlDatabase(params);
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
return createMongoDatabase(params);
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
return createCassandraKeyspace(params);
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
return createGremlineDatabase(params);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getSqlDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateSqlDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getMongoDBDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateMongoDBDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getCassandraKeyspace(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse?.properties?.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateCassandraKeyspace(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getGremlinDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateGremlinDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
const createBody: DatabaseRequest = { id: params.databaseId };
|
||||||
|
const databaseOptions: RequestOptions = {};
|
||||||
|
// TODO: replace when SDK support autopilot
|
||||||
|
if (params.databaseLevelThroughput) {
|
||||||
|
if (params.autoPilotMaxThroughput) {
|
||||||
|
createBody.maxThroughput = params.autoPilotMaxThroughput;
|
||||||
|
} else {
|
||||||
|
createBody.throughput = params.offerThroughput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: DatabaseResponse = await client().databases.create(createBody, databaseOptions);
|
||||||
|
return response.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructRpOptions(params: DataModels.CreateDatabaseParams): CreateUpdateOptions {
|
||||||
|
if (!params.databaseLevelThroughput) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.autoPilotMaxThroughput) {
|
||||||
|
return {
|
||||||
|
autoscaleSettings: {
|
||||||
|
maxThroughput: params.autoPilotMaxThroughput
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
throughput: params.offerThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,16 +1,46 @@
|
|||||||
jest.mock("../../Utils/arm/request");
|
jest.mock("../../Utils/arm/request");
|
||||||
jest.mock("../MessageHandler");
|
jest.mock("../MessageHandler");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
import { deleteCollection } from "./deleteCollection";
|
import { deleteCollection } from "./deleteCollection";
|
||||||
import { armRequest } from "../../Utils/arm/request";
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
import { sendCachedDataMessage } from "../MessageHandler";
|
import { sendCachedDataMessage } from "../MessageHandler";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
|
||||||
describe("deleteCollection", () => {
|
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 () => {
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
window.authType = AuthType.AAD;
|
window.authType = AuthType.AAD;
|
||||||
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
|
||||||
await deleteCollection("database", "collection");
|
await deleteCollection("database", "collection");
|
||||||
expect(armRequest).toHaveBeenCalled();
|
expect(armRequest).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
// TODO: Test non-AAD case
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,31 +1,57 @@
|
|||||||
import { CosmosClient } from "../CosmosClient";
|
|
||||||
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
|
||||||
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
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> {
|
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||||
try {
|
try {
|
||||||
if (window.authType === AuthType.AAD) {
|
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||||
await deleteSqlContainer(
|
await deleteCollectionWithARM(databaseId, collectionId);
|
||||||
CosmosClient.subscriptionId(),
|
|
||||||
CosmosClient.resourceGroup(),
|
|
||||||
CosmosClient.databaseAccount().name,
|
|
||||||
databaseId,
|
|
||||||
collectionId
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await CosmosClient.client()
|
await client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
.container(collectionId)
|
.container(collectionId)
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
|
logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "DeleteCollection", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
||||||
clearMessage();
|
clearMessage();
|
||||||
await refreshCachedResources();
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,42 @@
|
|||||||
jest.mock("../../Utils/arm/request");
|
jest.mock("../../Utils/arm/request");
|
||||||
jest.mock("../MessageHandler");
|
jest.mock("../MessageHandler");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
import { deleteDatabase } from "./deleteDatabase";
|
import { deleteDatabase } from "./deleteDatabase";
|
||||||
import { armRequest } from "../../Utils/arm/request";
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
import { sendCachedDataMessage } from "../MessageHandler";
|
import { sendCachedDataMessage } from "../MessageHandler";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
|
||||||
describe("deleteDatabase", () => {
|
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 () => {
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
window.authType = AuthType.AAD;
|
window.authType = AuthType.AAD;
|
||||||
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
|
||||||
await deleteDatabase("database");
|
await deleteDatabase("database");
|
||||||
expect(armRequest).toHaveBeenCalled();
|
expect(armRequest).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
// TODO: Test non-AAD case
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { CosmosClient } from "../CosmosClient";
|
|
||||||
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
|
||||||
import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
|
||||||
import { AuthType } from "../../AuthType";
|
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 { logError } from "../Logger";
|
||||||
import { sendNotificationForError } from "./sendNotificationForError";
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
|
||||||
@@ -10,15 +15,14 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
|||||||
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
|
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.authType === AuthType.AAD) {
|
if (
|
||||||
await deleteSqlDatabase(
|
window.authType === AuthType.AAD &&
|
||||||
CosmosClient.subscriptionId(),
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
|
||||||
CosmosClient.resourceGroup(),
|
!userContext.useSDKOperations
|
||||||
CosmosClient.databaseAccount().name,
|
) {
|
||||||
databaseId
|
await deleteDatabaseWithARM(databaseId);
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await CosmosClient.client()
|
await client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
@@ -32,3 +36,23 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
|||||||
clearMessage();
|
clearMessage();
|
||||||
await refreshCachedResources();
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
35
src/Common/dataAccess/readCollection.test.ts
Normal file
35
src/Common/dataAccess/readCollection.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
jest.mock("../CosmosClient");
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { readCollection } from "./readCollection";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
describe("readCollection", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "test"
|
||||||
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call SDK if logged in with resource token", async () => {
|
||||||
|
window.authType = AuthType.ResourceToken;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
database: () => {
|
||||||
|
return {
|
||||||
|
container: () => {
|
||||||
|
return {
|
||||||
|
read: (): unknown => ({})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await readCollection("database", "collection");
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
24
src/Common/dataAccess/readCollection.ts
Normal file
24
src/Common/dataAccess/readCollection.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
|
||||||
|
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
|
||||||
|
let collection: DataModels.Collection;
|
||||||
|
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
|
||||||
|
try {
|
||||||
|
const response = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.read();
|
||||||
|
collection = response.resource as DataModels.Collection;
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while querying container ${collectionId}:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "ReadCollection", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
clearMessage();
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
45
src/Common/dataAccess/readCollections.test.ts
Normal file
45
src/Common/dataAccess/readCollections.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
jest.mock("../../Utils/arm/request");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { readCollections } from "./readCollections";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
describe("readCollections", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "test"
|
||||||
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
await readCollections("database");
|
||||||
|
expect(armRequest).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call SDK if not logged in with non-AAD method", async () => {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
database: () => {
|
||||||
|
return {
|
||||||
|
containers: {
|
||||||
|
readAll: () => {
|
||||||
|
return {
|
||||||
|
fetchAll: (): unknown => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await readCollections("database");
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
71
src/Common/dataAccess/readCollections.ts
Normal file
71
src/Common/dataAccess/readCollections.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
|
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
|
let collections: DataModels.Collection[];
|
||||||
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
!userContext.useSDKOperations &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
|
) {
|
||||||
|
collections = await readCollectionsWithARM(databaseId);
|
||||||
|
} else {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.containers.readAll()
|
||||||
|
.fetchAll();
|
||||||
|
collections = sdkResponse.resources as DataModels.Collection[];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "ReadCollections", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
clearMessage();
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
|
let rpResponse;
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
rpResponse = await listSqlContainers(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
rpResponse = await listMongoDBCollections(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
rpResponse = await listCassandraTables(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
rpResponse = await listGremlinGraphs(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Table:
|
||||||
|
rpResponse = await listTables(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpResponse?.value?.map(collection => collection.properties?.resource as DataModels.Collection);
|
||||||
|
}
|
||||||
41
src/Common/dataAccess/readDatabases.test.ts
Normal file
41
src/Common/dataAccess/readDatabases.test.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
jest.mock("../../Utils/arm/request");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { readDatabases } from "./readDatabases";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
describe("readDatabases", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "test"
|
||||||
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
await readDatabases();
|
||||||
|
expect(armRequest).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call SDK if not logged in with non-AAD method", async () => {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
databases: {
|
||||||
|
readAll: () => {
|
||||||
|
return {
|
||||||
|
fetchAll: (): unknown => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await readDatabases();
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
67
src/Common/dataAccess/readDatabases.ts
Normal file
67
src/Common/dataAccess/readDatabases.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||||
|
let databases: DataModels.Database[];
|
||||||
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
!userContext.useSDKOperations &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
|
||||||
|
) {
|
||||||
|
databases = await readDatabasesWithARM();
|
||||||
|
} else {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.databases.readAll()
|
||||||
|
.fetchAll();
|
||||||
|
databases = sdkResponse.resources as DataModels.Database[];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while querying databases:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "ReadDatabases", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
clearMessage();
|
||||||
|
return databases;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
|
||||||
|
let rpResponse;
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpResponse?.value?.map(database => database.properties?.resource as DataModels.Database);
|
||||||
|
}
|
||||||
225
src/Common/dataAccess/updateCollection.ts
Normal file
225
src/Common/dataAccess/updateCollection.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { Collection } from "../../Contracts/DataModels";
|
||||||
|
import { ContainerDefinition } from "@azure/cosmos";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import {
|
||||||
|
ExtendedResourceProperties,
|
||||||
|
SqlContainerCreateUpdateParameters,
|
||||||
|
SqlContainerResource
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import {
|
||||||
|
createUpdateCassandraTable,
|
||||||
|
getCassandraTable
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import {
|
||||||
|
createUpdateMongoDBCollection,
|
||||||
|
getMongoDBCollection
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import {
|
||||||
|
createUpdateGremlinGraph,
|
||||||
|
getGremlinGraph
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function updateCollection(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
newCollection: Collection,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<Collection> {
|
||||||
|
let collection: Collection;
|
||||||
|
const clearMessage = logConsoleProgress(`Updating container ${collectionId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
|
) {
|
||||||
|
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||||
|
} else {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.replace(newCollection as ContainerDefinition, options);
|
||||||
|
collection = sdkResponse.resource as Collection;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "UpdateCollection", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logConsoleInfo(`Successfully updated container ${collectionId}`);
|
||||||
|
clearMessage();
|
||||||
|
await refreshCachedResources();
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCollectionWithARM(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
return updateSqlContainer(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
return updateMongoDBCollection(
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
newCollection
|
||||||
|
);
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
return updateCassandraTable(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
case DefaultAccountExperienceType.Table:
|
||||||
|
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSqlContainer(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateSqlContainer(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateMongoDBCollection(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateMongoDBCollection(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`MongoDB collection to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCassandraTable(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateCassandraTable(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Cassandra table to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGremlinGraph(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateGremlinGraph(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Gremlin graph to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTable(
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateTable(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Table to update does not exist. Table id: ${collectionId}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
||||||
|
|
||||||
|
describe("updateOfferThroughputBeyondLimit", () => {
|
||||||
|
it("should call fetch", async () => {
|
||||||
|
window.fetch = jest.fn(() => {
|
||||||
|
return {
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
window.dataExplorer = {
|
||||||
|
logConsoleData: jest.fn(),
|
||||||
|
deleteInProgressConsoleDataWithId: jest.fn(),
|
||||||
|
extensionEndpoint: jest.fn()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} as any;
|
||||||
|
await updateOfferThroughputBeyondLimit({
|
||||||
|
subscriptionId: "foo",
|
||||||
|
resourceGroup: "foo",
|
||||||
|
databaseAccountName: "foo",
|
||||||
|
databaseName: "foo",
|
||||||
|
throughput: 1000000000,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
|
});
|
||||||
|
expect(window.fetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
52
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
52
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Platform, configContext } from "../../ConfigContext";
|
||||||
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
|
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
||||||
|
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
|
||||||
|
interface UpdateOfferThroughputRequest {
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
databaseAccountName: string;
|
||||||
|
databaseName: string;
|
||||||
|
collectionName?: string;
|
||||||
|
throughput: number;
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||||
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
||||||
|
if (configContext.platform !== Platform.Portal) {
|
||||||
|
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceDescriptionInfo = request.collectionName
|
||||||
|
? `database ${request.databaseName} and container ${request.collectionName}`
|
||||||
|
: `database ${request.databaseName}`;
|
||||||
|
|
||||||
|
const clearMessage = logConsoleProgress(
|
||||||
|
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const explorer = window.dataExplorer;
|
||||||
|
const url = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
||||||
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
logConsoleInfo(
|
||||||
|
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||||
|
);
|
||||||
|
clearMessage();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const error = await response.json();
|
||||||
|
logConsoleError(`Failed to request an increase in throughput for ${request.throughput}: ${error.message}`);
|
||||||
|
clearMessage();
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ export enum Platform {
|
|||||||
Emulator = "Emulator"
|
Emulator = "Emulator"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Config {
|
interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedParentFrameOrigins: RegExp;
|
allowedParentFrameOrigins: RegExp;
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
@@ -28,7 +28,7 @@ interface Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
let config: Config = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
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$/,
|
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
|
// Webpack injects this at build time
|
||||||
@@ -46,36 +46,58 @@ let config: Config = {
|
|||||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
|
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
|
// Injected for local develpment. These will be removed in the production bundle by webpack
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
const port: string = process.env.PORT || "1234";
|
const port: string = process.env.PORT || "1234";
|
||||||
config.BACKEND_ENDPOINT = "https://localhost:" + port;
|
updateConfigContext({
|
||||||
config.MONGO_BACKEND_ENDPOINT = "https://localhost:" + port;
|
BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||||
config.PROXY_PATH = "/proxy";
|
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||||
config.EMULATOR_ENDPOINT = "https://localhost:8081";
|
PROXY_PATH: "/proxy",
|
||||||
|
EMULATOR_ENDPOINT: "https://localhost:8081"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeConfiguration(): Promise<Config> {
|
export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("./config.json");
|
const response = await fetch("./config.json");
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
try {
|
try {
|
||||||
const externalConfig = await response.json();
|
const externalConfig = await response.json();
|
||||||
config = Object.assign({}, config, externalConfig);
|
Object.assign(configContext, externalConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to parse json in config file");
|
console.error("Unable to parse json in config file");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Allow override of any config value with URL query parameters
|
// Allow override of platform value with URL query parameter
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
params.forEach((value, key) => {
|
if (params.has("platform")) {
|
||||||
(config as any)[key] = value;
|
const platform = params.get("platform");
|
||||||
});
|
switch (platform) {
|
||||||
|
default:
|
||||||
|
console.log("Invalid platform query parameter given, ignoring");
|
||||||
|
break;
|
||||||
|
case Platform.Portal:
|
||||||
|
case Platform.Hosted:
|
||||||
|
case Platform.Emulator:
|
||||||
|
updateConfigContext({ platform });
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("No configuration file found using defaults");
|
console.log("No configuration file found using defaults");
|
||||||
}
|
}
|
||||||
return config;
|
return configContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { config };
|
export { configContext };
|
||||||
@@ -153,7 +153,14 @@ export interface KeyResource {
|
|||||||
Token: string;
|
Token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndexingPolicy {}
|
export interface IndexingPolicy {
|
||||||
|
automatic: boolean;
|
||||||
|
indexingMode: string;
|
||||||
|
includedPaths: any;
|
||||||
|
excludedPaths: any;
|
||||||
|
compositeIndexes?: any;
|
||||||
|
spatialIndexes?: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PartitionKey {
|
export interface PartitionKey {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
@@ -312,17 +319,6 @@ export interface Query {
|
|||||||
query: string;
|
query: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateOfferThroughputRequest {
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroup: string;
|
|
||||||
databaseAccountName: string;
|
|
||||||
databaseName: string;
|
|
||||||
collectionName: string;
|
|
||||||
throughput: number;
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutoPilotOfferSettings {
|
export interface AutoPilotOfferSettings {
|
||||||
tier?: AutopilotTier;
|
tier?: AutopilotTier;
|
||||||
maximumTierThroughput?: number;
|
maximumTierThroughput?: number;
|
||||||
@@ -331,12 +327,11 @@ export interface AutoPilotOfferSettings {
|
|||||||
targetMaxThroughput?: number;
|
targetMaxThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateDatabaseRequest {
|
export interface CreateDatabaseParams {
|
||||||
|
autoPilotMaxThroughput?: number;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput?: boolean;
|
databaseLevelThroughput?: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
autoPilot?: AutoPilotCreationSettings;
|
|
||||||
hasAutoPilotV2FeatureFlag?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharedThroughputRange {
|
export interface SharedThroughputRange {
|
||||||
|
|||||||
8
src/DefaultAccountExperienceType.ts
Normal file
8
src/DefaultAccountExperienceType.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export enum DefaultAccountExperienceType {
|
||||||
|
DocumentDB = "DocumentDB",
|
||||||
|
Graph = "Graph",
|
||||||
|
MongoDB = "MongoDB",
|
||||||
|
Table = "Table",
|
||||||
|
Cassandra = "Cassandra",
|
||||||
|
ApiForMongoDB = "Azure Cosmos DB for MongoDB API"
|
||||||
|
}
|
||||||
@@ -49,6 +49,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
|||||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", 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.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||||
{
|
{
|
||||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||||
|
|||||||
@@ -163,8 +163,14 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.canexceedmaximumvalue"
|
key="feature.enablecodeofconduct"
|
||||||
label="Can exceed max value"
|
label="Enable Code Of Conduct Acknowledgement"
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
checked={false}
|
||||||
|
key="feature.enableLinkInjection"
|
||||||
|
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -172,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
className="checkboxRow"
|
className="checkboxRow"
|
||||||
horizontalAlign="space-between"
|
horizontalAlign="space-between"
|
||||||
>
|
>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
checked={false}
|
||||||
|
key="feature.canexceedmaximumvalue"
|
||||||
|
label="Can exceed max value"
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ describe("GalleryCardComponent", () => {
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
},
|
},
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
showDownload: true,
|
showDownload: true,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import * as sinon from "sinon";
|
||||||
|
import React from "react";
|
||||||
|
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
|
||||||
|
import { IJunoResponse, JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||||
|
|
||||||
|
describe("CodeOfConductComponent", () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
let codeOfConductProps: CodeOfConductComponentProps;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
sandbox.stub(JunoClient.prototype, "acceptCodeOfConduct").returns({
|
||||||
|
status: HttpStatusCodes.OK,
|
||||||
|
data: true
|
||||||
|
} as IJunoResponse<boolean>);
|
||||||
|
const junoClient = new JunoClient(undefined);
|
||||||
|
codeOfConductProps = {
|
||||||
|
junoClient: junoClient,
|
||||||
|
onAcceptCodeOfConduct: jest.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onAcceptedCodeOfConductCalled", async () => {
|
||||||
|
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
|
||||||
|
wrapper
|
||||||
|
.find(".genericPaneSubmitBtn")
|
||||||
|
.first()
|
||||||
|
.simulate("click");
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
112
src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx
Normal file
112
src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
||||||
|
import * as Logger from "../../../Common/Logger";
|
||||||
|
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||||
|
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
export interface CodeOfConductComponentProps {
|
||||||
|
junoClient: JunoClient;
|
||||||
|
onAcceptCodeOfConduct: (result: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeOfConductComponentState {
|
||||||
|
readCodeOfConduct: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
|
||||||
|
private descriptionPara1: string;
|
||||||
|
private descriptionPara2: string;
|
||||||
|
private descriptionPara3: string;
|
||||||
|
private link1: { label: string; url: string };
|
||||||
|
private link2: { label: string; url: string };
|
||||||
|
|
||||||
|
constructor(props: CodeOfConductComponentProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
readCodeOfConduct: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
|
||||||
|
this.descriptionPara2 =
|
||||||
|
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
|
||||||
|
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
|
||||||
|
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
|
||||||
|
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async acceptCodeOfConduct(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await this.props.junoClient.acceptCodeOfConduct();
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
|
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onAcceptCodeOfConduct(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
const message = `Failed to accept code of conduct: ${error}`;
|
||||||
|
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
|
||||||
|
logConsoleError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onChangeCheckbox = (): void => {
|
||||||
|
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 20 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Text>{this.descriptionPara2}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Text>
|
||||||
|
{this.descriptionPara3}
|
||||||
|
<Link href={this.link1.url} target="_blank">
|
||||||
|
{this.link1.label}
|
||||||
|
</Link>
|
||||||
|
{" and "}
|
||||||
|
<Link href={this.link2.url} target="_blank">
|
||||||
|
{this.link2.label}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Checkbox
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
margin: 0,
|
||||||
|
padding: "2 0 2 0"
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label="I have read and accepted the code of conduct and privacy statement"
|
||||||
|
onChange={this.onChangeCheckbox}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<PrimaryButton
|
||||||
|
ariaLabel="Continue"
|
||||||
|
title="Continue"
|
||||||
|
onClick={async () => await this.acceptCodeOfConduct()}
|
||||||
|
tabIndex={0}
|
||||||
|
className="genericPaneSubmitBtn"
|
||||||
|
text="Continue"
|
||||||
|
disabled={!this.state.readCodeOfConduct}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Logger from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
||||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
@@ -24,6 +24,8 @@ import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/Gallery
|
|||||||
import "./GalleryViewerComponent.less";
|
import "./GalleryViewerComponent.less";
|
||||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
||||||
|
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||||
|
|
||||||
export interface GalleryViewerComponentProps {
|
export interface GalleryViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
@@ -60,6 +62,7 @@ interface GalleryViewerComponentState {
|
|||||||
sortBy: SortBy;
|
sortBy: SortBy;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
dialogProps: DialogProps;
|
dialogProps: DialogProps;
|
||||||
|
isCodeOfConductAccepted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GalleryTabInfo {
|
interface GalleryTabInfo {
|
||||||
@@ -86,6 +89,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private publicNotebooks: IGalleryItem[];
|
private publicNotebooks: IGalleryItem[];
|
||||||
private favoriteNotebooks: IGalleryItem[];
|
private favoriteNotebooks: IGalleryItem[];
|
||||||
private publishedNotebooks: IGalleryItem[];
|
private publishedNotebooks: IGalleryItem[];
|
||||||
|
private isCodeOfConductAccepted: boolean;
|
||||||
private columnCount: number;
|
private columnCount: number;
|
||||||
private rowCount: number;
|
private rowCount: number;
|
||||||
|
|
||||||
@@ -100,7 +104,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
selectedTab: props.selectedTab,
|
selectedTab: props.selectedTab,
|
||||||
sortBy: props.sortBy,
|
sortBy: props.sortBy,
|
||||||
searchText: props.searchText,
|
searchText: props.searchText,
|
||||||
dialogProps: undefined
|
dialogProps: undefined,
|
||||||
|
isCodeOfConductAccepted: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sortingOptions = [
|
this.sortingOptions = [
|
||||||
@@ -134,9 +139,20 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
|
|
||||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
|
tabs.push(
|
||||||
|
this.createPublicGalleryTab(
|
||||||
|
GalleryTab.PublicGallery,
|
||||||
|
this.state.publicNotebooks,
|
||||||
|
this.state.isCodeOfConductAccepted
|
||||||
|
)
|
||||||
|
);
|
||||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
|
||||||
|
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||||
|
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||||
|
if (this.state.isCodeOfConductAccepted !== false) {
|
||||||
|
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
@@ -167,6 +183,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPublicGalleryTab(
|
||||||
|
tab: GalleryTab,
|
||||||
|
data: IGalleryItem[],
|
||||||
|
acceptedCodeOfConduct: boolean
|
||||||
|
): GalleryTabInfo {
|
||||||
|
return {
|
||||||
|
tab,
|
||||||
|
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
@@ -174,6 +201,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
|
||||||
|
return acceptedCodeOfConduct === false ? (
|
||||||
|
<CodeOfConductComponent
|
||||||
|
junoClient={this.props.junoClient}
|
||||||
|
onAcceptCodeOfConduct={(result: boolean) => {
|
||||||
|
this.setState({ isCodeOfConductAccepted: result });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
this.createTabContent(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
@@ -187,8 +227,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
||||||
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
{this.props.container?.isGalleryPublishEnabled() && (
|
||||||
|
<Stack.Item>
|
||||||
|
<InfoComponent />
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{data && this.createCardsTabContent(data)}
|
{data && this.createCardsTabContent(data)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -254,12 +298,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
try {
|
try {
|
||||||
const response = await this.props.junoClient.getPublicNotebooks();
|
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
||||||
|
if (this.props.container.isCodeOfConductEnabled()) {
|
||||||
|
response = await this.props.junoClient.fetchPublicNotebooks();
|
||||||
|
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||||
|
this.publicNotebooks = response.data?.notebooksData;
|
||||||
|
} else {
|
||||||
|
response = await this.props.junoClient.getPublicNotebooks();
|
||||||
|
this.publicNotebooks = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.publicNotebooks = response.data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to load public notebooks: ${error}`;
|
const message = `Failed to load public notebooks: ${error}`;
|
||||||
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
||||||
@@ -268,7 +319,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
|
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
|
||||||
|
isCodeOfConductAccepted: this.isCodeOfConductAccepted
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,12 +385,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
||||||
const toSearch = searchText.trim().toUpperCase();
|
const toSearch = searchText.trim().toUpperCase();
|
||||||
const searchData: string[] = [
|
const searchData: string[] = [item.author.toUpperCase(), item.description.toUpperCase(), item.name.toUpperCase()];
|
||||||
item.author.toUpperCase(),
|
|
||||||
item.description.toUpperCase(),
|
if (item.tags) {
|
||||||
item.name.toUpperCase(),
|
searchData.push(...item.tags.map(tag => tag.toUpperCase()));
|
||||||
...item.tags?.map(tag => tag.toUpperCase())
|
}
|
||||||
];
|
|
||||||
|
|
||||||
for (const data of searchData) {
|
for (const data of searchData) {
|
||||||
if (data?.indexOf(toSearch) !== -1) {
|
if (data?.indexOf(toSearch) !== -1) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@import "../../../../../less/Common/Constants.less";
|
||||||
|
.infoPanel, .infoPanelMain {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoPanel {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel, .infoLabelMain {
|
||||||
|
padding-left: 5px
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
font-weight: 400
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoIconMain {
|
||||||
|
color: @AccentMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoIconMain:hover {
|
||||||
|
color: @BaseMedium
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { InfoComponent } from "./InfoComponent";
|
||||||
|
|
||||||
|
describe("InfoComponent", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
const wrapper = shallow(<InfoComponent />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fabric-react";
|
||||||
|
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
||||||
|
import "./InfoComponent.less";
|
||||||
|
|
||||||
|
export class InfoComponent extends React.Component {
|
||||||
|
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Link href={url} target="_blank">
|
||||||
|
<div className="infoPanel">
|
||||||
|
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
|
||||||
|
<Label className="infoLabel">{labelText}</Label>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onHover = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 5, padding: 5 }}>
|
||||||
|
<Stack.Item>{this.getInfoPanel("Script", "Code of Conduct", CodeOfConductEndpoints.codeOfConduct)}</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{this.getInfoPanel("RedEye", "Privacy Statement", CodeOfConductEndpoints.privacyStatement)}
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
|
||||||
|
<div className="infoPanelMain">
|
||||||
|
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
|
||||||
|
<Label className="infoLabelMain">Help</Label>
|
||||||
|
</div>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`InfoComponent renders 1`] = `
|
||||||
|
<StyledHoverCardBase
|
||||||
|
instantOpenOnClick={true}
|
||||||
|
plainCardProps={
|
||||||
|
Object {
|
||||||
|
"onRenderPlainCard": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="PlainCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="infoPanelMain"
|
||||||
|
>
|
||||||
|
<Memo(StyledIconBase)
|
||||||
|
className="infoIconMain"
|
||||||
|
iconName="Help"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledLabelBase
|
||||||
|
className="infoLabelMain"
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</StyledLabelBase>
|
||||||
|
</div>
|
||||||
|
</StyledHoverCardBase>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CodeOfConductComponent renders 1`] = `
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "20px",
|
||||||
|
"fontWeight": 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<Text>
|
||||||
|
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<Text>
|
||||||
|
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/cosmos-code-of-conduct"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
code of conduct
|
||||||
|
</StyledLinkBase>
|
||||||
|
and
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/ms-privacy-policy"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
privacy statement
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
label="I have read and accepted the code of conduct and privacy statement"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"margin": 0,
|
||||||
|
"padding": "2 0 2 0",
|
||||||
|
},
|
||||||
|
"text": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
ariaLabel="Continue"
|
||||||
|
className="genericPaneSubmitBtn"
|
||||||
|
disabled={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
text="Continue"
|
||||||
|
title="Continue"
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
@@ -17,7 +17,8 @@ describe("NotebookMetadataComponent", () => {
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
},
|
},
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
downloadButtonText: "Download",
|
downloadButtonText: "Download",
|
||||||
@@ -45,7 +46,8 @@ describe("NotebookMetadataComponent", () => {
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
},
|
},
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
downloadButtonText: "Download",
|
downloadButtonText: "Download",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComp
|
|||||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||||
import "./NotebookViewerComponent.less";
|
import "./NotebookViewerComponent.less";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
||||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||||
|
|
||||||
export interface NotebookViewerComponentProps {
|
export interface NotebookViewerComponentProps {
|
||||||
@@ -86,6 +87,7 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notebook: Notebook = await response.json();
|
const notebook: Notebook = await response.json();
|
||||||
|
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
|
||||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||||
this.setState({ content: notebook, showProgressBar: false });
|
this.setState({ content: notebook, showProgressBar: false });
|
||||||
|
|
||||||
@@ -105,10 +107,21 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
|
||||||
|
if (!newCellId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notebookV4 = notebook as NotebookV4;
|
||||||
|
if (notebookV4 && notebookV4.cells[0].source[0].search(newCellId)) {
|
||||||
|
delete notebookV4.cells[0];
|
||||||
|
notebook = notebookV4;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="notebookViewerContainer">
|
<div className="notebookViewerContainer">
|
||||||
{this.props.backNavigationText ? (
|
{this.props.backNavigationText !== undefined ? (
|
||||||
<Link onClick={this.props.onBackClick}>
|
<Link onClick={this.props.onBackClick}>
|
||||||
<Icon iconName="Back" /> {this.props.backNavigationText}
|
<Icon iconName="Back" /> {this.props.backNavigationText}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import * as sinon from "sinon";
|
|||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
|
import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
|
||||||
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
describe("ContainerSampleGenerator", () => {
|
describe("ContainerSampleGenerator", () => {
|
||||||
const createExplorerStub = (database: ViewModels.Database): Explorer => {
|
const createExplorerStub = (database: ViewModels.Database): Explorer => {
|
||||||
@@ -75,8 +75,21 @@ describe("ContainerSampleGenerator", () => {
|
|||||||
sinon.stub(GremlinClient.prototype, "initialize").callsFake(() => {});
|
sinon.stub(GremlinClient.prototype, "initialize").callsFake(() => {});
|
||||||
const executeStub = sinon.stub(GremlinClient.prototype, "execute").returns(Q.resolve());
|
const executeStub = sinon.stub(GremlinClient.prototype, "execute").returns(Q.resolve());
|
||||||
|
|
||||||
sinon.stub(CosmosClient, "databaseAccount").returns({
|
updateUserContext({
|
||||||
properties: {}
|
databaseAccount: {
|
||||||
|
id: "foo",
|
||||||
|
name: "foo",
|
||||||
|
location: "foo",
|
||||||
|
type: "foo",
|
||||||
|
kind: "foo",
|
||||||
|
tags: [],
|
||||||
|
properties: {
|
||||||
|
documentEndpoint: "bar",
|
||||||
|
gremlinEndpoint: "foo",
|
||||||
|
tableEndpoint: "foo",
|
||||||
|
cassandraEndpoint: "foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const sampleCollectionId = "SampleCollection";
|
const sampleCollectionId = "SampleCollection";
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import * as DataModels from "../../Contracts/DataModels";
|
|||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import GraphTab from ".././Tabs/GraphTab";
|
import GraphTab from ".././Tabs/GraphTab";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { createDocument, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
|
import { createDocument, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
|
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
|
||||||
data: any[];
|
data: any[];
|
||||||
@@ -87,14 +87,14 @@ export class ContainerSampleGenerator {
|
|||||||
if (!queries || queries.length < 1) {
|
if (!queries || queries.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const account = CosmosClient.databaseAccount();
|
const account = userContext.databaseAccount;
|
||||||
const databaseId = collection.databaseId;
|
const databaseId = collection.databaseId;
|
||||||
const gremlinClient = new GremlinClient();
|
const gremlinClient = new GremlinClient();
|
||||||
gremlinClient.initialize({
|
gremlinClient.initialize({
|
||||||
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
||||||
databaseId: databaseId,
|
databaseId: databaseId,
|
||||||
collectionId: collection.id(),
|
collectionId: collection.id(),
|
||||||
masterKey: CosmosClient.masterKey() || "",
|
masterKey: userContext.masterKey || "",
|
||||||
maxResultSize: 100
|
maxResultSize: 100
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
|||||||
import Database from "./Tree/Database";
|
import Database from "./Tree/Database";
|
||||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||||
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
||||||
import { readDatabases, readCollection, readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
||||||
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||||
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
||||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||||
@@ -35,9 +37,8 @@ import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer
|
|||||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
|
||||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||||
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { config } from "../Config";
|
import { configContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { CosmosClient } from "../Common/CosmosClient";
|
|
||||||
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||||
import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter";
|
import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter";
|
||||||
@@ -85,6 +86,7 @@ import { NotificationsClientBase } from "../Common/NotificationsClientBase";
|
|||||||
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
||||||
import TabsBase from "./Tabs/TabsBase";
|
import TabsBase from "./Tabs/TabsBase";
|
||||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||||
@@ -203,11 +205,15 @@ export default class Explorer {
|
|||||||
public setupNotebooksPane: SetupNotebooksPane;
|
public setupNotebooksPane: SetupNotebooksPane;
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: ReactAdapter;
|
public publishNotebookPaneAdapter: ReactAdapter;
|
||||||
|
public copyNotebookPaneAdapter: ReactAdapter;
|
||||||
|
|
||||||
// features
|
// features
|
||||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
|
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||||
|
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
|
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
@@ -408,8 +414,15 @@ export default class Explorer {
|
|||||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
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.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
@@ -471,7 +484,13 @@ export default class Explorer {
|
|||||||
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
|
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
|
||||||
this.defaultExperience = ko.observable<string>();
|
this.defaultExperience = ko.observable<string>();
|
||||||
this.databaseAccount.subscribe(databaseAccount => {
|
this.databaseAccount.subscribe(databaseAccount => {
|
||||||
this.defaultExperience(DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount));
|
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
|
||||||
|
databaseAccount
|
||||||
|
);
|
||||||
|
this.defaultExperience(defaultExperience);
|
||||||
|
updateUserContext({
|
||||||
|
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isPreferredApiDocumentDB = ko.computed(() => {
|
this.isPreferredApiDocumentDB = ko.computed(() => {
|
||||||
@@ -956,6 +975,10 @@ export default class Explorer {
|
|||||||
this.sparkClusterConnectionInfo.valueHasMutated();
|
this.sparkClusterConnectionInfo.valueHasMutated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
|
||||||
|
updateUserContext({ useSDKOperations: true });
|
||||||
|
}
|
||||||
|
|
||||||
featureSubcription.dispose();
|
featureSubcription.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1403,7 +1426,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
||||||
this._setLoadingStatusText("Fetching databases...");
|
this._setLoadingStatusText("Fetching databases...");
|
||||||
readDatabases(null /*options*/).then(
|
readDatabases().then(
|
||||||
(databases: DataModels.Database[]) => {
|
(databases: DataModels.Database[]) => {
|
||||||
this._setLoadingStatusText("Successfully fetched databases.");
|
this._setLoadingStatusText("Successfully fetched databases.");
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
@@ -1456,38 +1479,33 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isServerlessEnabled()) {
|
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
|
||||||
// Serverless accounts don't support offers call
|
this._setLoadingStatusText("Fetching offers...");
|
||||||
refreshDatabases();
|
offerPromise.then(
|
||||||
} else {
|
(offers: DataModels.Offer[]) => {
|
||||||
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers();
|
this._setLoadingStatusText("Successfully fetched offers.");
|
||||||
this._setLoadingStatusText("Fetching offers...");
|
refreshDatabases(offers);
|
||||||
offerPromise.then(
|
},
|
||||||
(offers: DataModels.Offer[]) => {
|
error => {
|
||||||
this._setLoadingStatusText("Successfully fetched offers.");
|
this._setLoadingStatusText("Failed to fetch offers.");
|
||||||
refreshDatabases(offers);
|
this.isRefreshingExplorer(false);
|
||||||
},
|
deferred.reject(error);
|
||||||
error => {
|
TelemetryProcessor.traceFailure(
|
||||||
this._setLoadingStatusText("Failed to fetch offers.");
|
Action.LoadDatabases,
|
||||||
this.isRefreshingExplorer(false);
|
{
|
||||||
deferred.reject(error);
|
databaseAccountName: this.databaseAccount().name,
|
||||||
TelemetryProcessor.traceFailure(
|
defaultExperience: this.defaultExperience(),
|
||||||
Action.LoadDatabases,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
{
|
error: JSON.stringify(error)
|
||||||
databaseAccountName: this.databaseAccount().name,
|
},
|
||||||
defaultExperience: this.defaultExperience(),
|
startKey
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
);
|
||||||
error: JSON.stringify(error)
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
},
|
ConsoleDataType.Error,
|
||||||
startKey
|
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||||
);
|
);
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
}
|
||||||
ConsoleDataType.Error,
|
);
|
||||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deferred.promise.then(
|
return deferred.promise.then(
|
||||||
() => {
|
() => {
|
||||||
@@ -1603,7 +1621,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
private async _getArcadiaWorkspaces(): Promise<ArcadiaWorkspaceItem[]> {
|
private async _getArcadiaWorkspaces(): Promise<ArcadiaWorkspaceItem[]> {
|
||||||
try {
|
try {
|
||||||
const workspaces = await this._arcadiaManager.listWorkspacesAsync([CosmosClient.subscriptionId()]);
|
const workspaces = await this._arcadiaManager.listWorkspacesAsync([userContext.subscriptionId]);
|
||||||
let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length);
|
let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length);
|
||||||
const sparkPromises: Promise<void>[] = [];
|
const sparkPromises: Promise<void>[] = [];
|
||||||
workspaces.forEach((workspace, i) => {
|
workspaces.forEach((workspace, i) => {
|
||||||
@@ -1706,7 +1724,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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");
|
return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace");
|
Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace");
|
||||||
@@ -1719,6 +1737,7 @@ export default class Explorer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let clearMessage;
|
||||||
try {
|
try {
|
||||||
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
|
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
|
||||||
this.databaseAccount().id,
|
this.databaseAccount().id,
|
||||||
@@ -1730,10 +1749,14 @@ export default class Explorer {
|
|||||||
notebookWorkspace.properties.status &&
|
notebookWorkspace.properties.status &&
|
||||||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
||||||
) {
|
) {
|
||||||
|
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
||||||
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
|
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning");
|
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning");
|
||||||
|
NotificationConsoleUtils.logConsoleError(`Failed to initialize notebook workspace: ${JSON.stringify(error)}`);
|
||||||
|
} finally {
|
||||||
|
clearMessage && clearMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1808,8 +1831,8 @@ export default class Explorer {
|
|||||||
|
|
||||||
const isRunningInPortal = window.dataExplorerPlatform == PlatformType.Portal;
|
const isRunningInPortal = window.dataExplorerPlatform == PlatformType.Portal;
|
||||||
const isRunningInDevMode = process.env.NODE_ENV === "development";
|
const isRunningInDevMode = process.env.NODE_ENV === "development";
|
||||||
if (inputs && config.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
|
if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
|
||||||
inputs.extensionEndpoint = config.PROXY_PATH;
|
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
|
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
|
||||||
@@ -1914,7 +1937,7 @@ export default class Explorer {
|
|||||||
this.features(inputs.features);
|
this.features(inputs.features);
|
||||||
this.serverId(inputs.serverId);
|
this.serverId(inputs.serverId);
|
||||||
this.extensionEndpoint(inputs.extensionEndpoint || "");
|
this.extensionEndpoint(inputs.extensionEndpoint || "");
|
||||||
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || config.ARM_ENDPOINT));
|
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
|
||||||
this.notificationsClient.setExtensionEndpoint(this.extensionEndpoint());
|
this.notificationsClient.setExtensionEndpoint(this.extensionEndpoint());
|
||||||
this.databaseAccount(databaseAccount);
|
this.databaseAccount(databaseAccount);
|
||||||
this.subscriptionType(inputs.subscriptionType);
|
this.subscriptionType(inputs.subscriptionType);
|
||||||
@@ -1930,11 +1953,17 @@ export default class Explorer {
|
|||||||
|
|
||||||
this._importExplorerConfigComplete = true;
|
this._importExplorerConfigComplete = true;
|
||||||
|
|
||||||
CosmosClient.authorizationToken(authorizationToken);
|
updateConfigContext({
|
||||||
CosmosClient.masterKey(masterKey);
|
ARM_ENDPOINT: this.armEndpoint()
|
||||||
CosmosClient.databaseAccount(databaseAccount);
|
});
|
||||||
CosmosClient.subscriptionId(inputs.subscriptionId);
|
|
||||||
CosmosClient.resourceGroup(inputs.resourceGroup);
|
updateUserContext({
|
||||||
|
authorizationToken,
|
||||||
|
masterKey,
|
||||||
|
databaseAccount,
|
||||||
|
resourceGroup: inputs.resourceGroup,
|
||||||
|
subscriptionId: inputs.subscriptionId
|
||||||
|
});
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadDatabaseAccount,
|
Action.LoadDatabaseAccount,
|
||||||
{
|
{
|
||||||
@@ -2179,7 +2208,7 @@ export default class Explorer {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlPrefixWithKeyParam: string = `${config.hostedExplorerURL}?key=`;
|
const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`;
|
||||||
const currentActiveTab = this.tabsManager.activeTab();
|
const currentActiveTab = this.tabsManager.activeTab();
|
||||||
|
|
||||||
return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`;
|
return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`;
|
||||||
@@ -2291,7 +2320,7 @@ export default class Explorer {
|
|||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/uploadFile");
|
Logger.logError(error, "Explorer/uploadFile");
|
||||||
@@ -2346,14 +2375,28 @@ export default class Explorer {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void {
|
public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> {
|
||||||
if (this.notebookManager) {
|
if (this.notebookManager) {
|
||||||
this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
await this.notebookManager.openPublishNotebookPane(
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
parentDomElement,
|
||||||
|
this.isCodeOfConductEnabled(),
|
||||||
|
this.isLinkInjectionEnabled()
|
||||||
|
);
|
||||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||||
this.isPublishNotebookPaneEnabled(true);
|
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 {
|
public showOkModalDialog(title: string, msg: string): void {
|
||||||
this._dialogProps({
|
this._dialogProps({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
@@ -2455,14 +2498,14 @@ export default class Explorer {
|
|||||||
this.tabsManager.activateTab(notebookTab);
|
this.tabsManager.activateTab(notebookTab);
|
||||||
} else {
|
} else {
|
||||||
const options: NotebookTabOptions = {
|
const options: NotebookTabOptions = {
|
||||||
account: CosmosClient.databaseAccount(),
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||||
node: null,
|
node: null,
|
||||||
title: notebookContentItem.name,
|
title: notebookContentItem.name,
|
||||||
tabPath: notebookContentItem.path,
|
tabPath: notebookContentItem.path,
|
||||||
collection: null,
|
collection: null,
|
||||||
selfLink: null,
|
selfLink: null,
|
||||||
masterKey: CosmosClient.masterKey() || "",
|
masterKey: userContext.masterKey || "",
|
||||||
hashLocation: "notebooks",
|
hashLocation: "notebooks",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
@@ -2652,7 +2695,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
|
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
|
||||||
const subscriptionId = CosmosClient.subscriptionId();
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const armEndpoint = this.armEndpoint();
|
const armEndpoint = this.armEndpoint();
|
||||||
const authType = window.authType as AuthType;
|
const authType = window.authType as AuthType;
|
||||||
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||||
@@ -2681,7 +2724,7 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
|
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
|
||||||
const subscriptionId = CosmosClient.subscriptionId();
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const armEndpoint = this.armEndpoint();
|
const armEndpoint = this.armEndpoint();
|
||||||
const authType = window.authType as AuthType;
|
const authType = window.authType as AuthType;
|
||||||
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||||
@@ -2710,6 +2753,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.resourceTree.initialize();
|
await this.resourceTree.initialize();
|
||||||
|
this.notebookManager?.refreshPinnedRepos();
|
||||||
if (this.notebookToImport) {
|
if (this.notebookToImport) {
|
||||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||||
}
|
}
|
||||||
@@ -2900,7 +2944,7 @@ export default class Explorer {
|
|||||||
this.tabsManager.activateTab(terminalTab);
|
this.tabsManager.activateTab(terminalTab);
|
||||||
} else {
|
} else {
|
||||||
const newTab = new TerminalTab({
|
const newTab = new TerminalTab({
|
||||||
account: CosmosClient.databaseAccount(),
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.Terminal,
|
tabKind: ViewModels.CollectionTabKind.Terminal,
|
||||||
node: null,
|
node: null,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -2939,7 +2983,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
const newTab = new this.galleryTab.default({
|
const newTab = new this.galleryTab.default({
|
||||||
// GalleryTabOptions
|
// GalleryTabOptions
|
||||||
account: CosmosClient.databaseAccount(),
|
account: userContext.databaseAccount,
|
||||||
container: this,
|
container: this,
|
||||||
junoClient: this.notebookManager?.junoClient,
|
junoClient: this.notebookManager?.junoClient,
|
||||||
notebookUrl,
|
notebookUrl,
|
||||||
@@ -2986,7 +3030,7 @@ export default class Explorer {
|
|||||||
this.tabsManager.activateNewTab(notebookViewerTab);
|
this.tabsManager.activateNewTab(notebookViewerTab);
|
||||||
} else {
|
} else {
|
||||||
notebookViewerTab = new this.notebookViewerTab.default({
|
notebookViewerTab = new this.notebookViewerTab.default({
|
||||||
account: CosmosClient.databaseAccount(),
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.NotebookViewer,
|
tabKind: ViewModels.CollectionTabKind.NotebookViewer,
|
||||||
node: null,
|
node: null,
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
@@ -87,13 +87,31 @@ describe("getPkIdFromDocumentId", () => {
|
|||||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
|
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)", () => {
|
it("should create pkid pair from partitioned graph (pk as valid array value)", () => {
|
||||||
const doc = createFakeDoc({ id: "id", mypk: [{ id: "someid", _value: "pkvalue" }] });
|
const doc = createFakeDoc({ id: "id", mypk: [{ id: "someid", _value: "pkvalue" }] });
|
||||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
|
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should error if id is not a string", () => {
|
it("should error if id is not a string or number", () => {
|
||||||
const doc = createFakeDoc({ id: { foo: 1 } });
|
let doc = createFakeDoc({ id: { foo: 1 } });
|
||||||
|
try {
|
||||||
|
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (e) {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc = createFakeDoc({ id: true });
|
||||||
try {
|
try {
|
||||||
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
|
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
|
||||||
expect(true).toBe(false);
|
expect(true).toBe(false);
|
||||||
@@ -102,16 +120,8 @@ describe("getPkIdFromDocumentId", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should error if pk not string nor non-empty array", () => {
|
it("should error if pk is empty array", () => {
|
||||||
let doc = createFakeDoc({ mypk: { foo: 1 } });
|
let doc = createFakeDoc({ mypk: [] });
|
||||||
|
|
||||||
try {
|
|
||||||
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
|
|
||||||
} catch (e) {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
doc = createFakeDoc({ mypk: [] });
|
|
||||||
try {
|
try {
|
||||||
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
|
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
|
||||||
expect(true).toBe(false);
|
expect(true).toBe(false);
|
||||||
|
|||||||
@@ -1371,7 +1371,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
|
|
||||||
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
|
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
|
||||||
let pk = (d as any)[collectionPartitionKeyProperty];
|
let pk = (d as any)[collectionPartitionKeyProperty];
|
||||||
if (typeof pk !== "string") {
|
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
|
||||||
if (Array.isArray(pk) && pk.length > 0) {
|
if (Array.isArray(pk) && pk.length > 0) {
|
||||||
// pk is [{ id: 'id', _value: 'value' }]
|
// pk is [{ id: 'id', _value: 'value' }]
|
||||||
pk = pk[0]["_value"];
|
pk = pk[0]["_value"];
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
|||||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||||
import GitHubIcon from "../../../../images/github.svg";
|
import GitHubIcon from "../../../../images/github.svg";
|
||||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||||
import { config, Platform } from "../../../Config";
|
import { configContext, Platform } from "../../../ConfigContext";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ export class CommandBarComponentButtonFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
private static createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (config.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +469,7 @@ export class CommandBarComponentButtonFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
private static createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (config.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const label = "Enable Notebooks (Preview)";
|
const label = "Enable Notebooks (Preview)";
|
||||||
|
|||||||
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
import { Channels } from "@nteract/messaging";
|
||||||
|
import * as monaco from "./monaco";
|
||||||
|
import * as React from "react";
|
||||||
|
import { completionProvider } from "./completions/completionItemProvider";
|
||||||
|
import { AppState, ContentRef } from "@nteract/core";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import "./styles.css";
|
||||||
|
import { LightThemeName, HCLightThemeName, DarkThemeName } from "./theme";
|
||||||
|
// import { logger } from "src/common/localLogger";
|
||||||
|
import { getCellMonacoLanguage } from "./selectors";
|
||||||
|
// import { DocumentUri } from "./documentUri";
|
||||||
|
|
||||||
|
export type IModelContentChangedEvent = monaco.editor.IModelContentChangedEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial props for Monaco received from agnostic component
|
||||||
|
*/
|
||||||
|
export interface IMonacoProps {
|
||||||
|
id: string;
|
||||||
|
contentRef: ContentRef;
|
||||||
|
modelUri?: monaco.Uri;
|
||||||
|
theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string;
|
||||||
|
cellLanguageOverride?: string;
|
||||||
|
notebookLanguageOverride?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
channels: Channels | undefined;
|
||||||
|
enableCompletion: boolean;
|
||||||
|
shouldRegisterDefaultCompletion?: boolean;
|
||||||
|
onChange: (value: string, event?: unknown) => void;
|
||||||
|
onFocusChange: (focus: boolean) => void;
|
||||||
|
onCursorPositionChange?: (selection: monaco.ISelection | null) => void;
|
||||||
|
onRegisterCompletionProvider?: (languageId: string) => void;
|
||||||
|
value: string;
|
||||||
|
editorFocused: boolean;
|
||||||
|
lineNumbers: boolean;
|
||||||
|
|
||||||
|
/** set height of editor to fit the specified number of lines in display */
|
||||||
|
numberOfLines?: number;
|
||||||
|
|
||||||
|
options?: monaco.editor.IEditorOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monaco specific props derived from State
|
||||||
|
*/
|
||||||
|
interface IMonacoStateProps {
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the custom theme data to avoid repeatly defining the custom theme
|
||||||
|
let customThemeData: monaco.editor.IStandaloneThemeData;
|
||||||
|
|
||||||
|
function getMonacoTheme(theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string) {
|
||||||
|
if (typeof theme === "string") {
|
||||||
|
switch (theme) {
|
||||||
|
case "vs-dark":
|
||||||
|
return DarkThemeName;
|
||||||
|
case "hc-black":
|
||||||
|
return "hc-black";
|
||||||
|
case "vs":
|
||||||
|
return LightThemeName;
|
||||||
|
case "hc-light":
|
||||||
|
return HCLightThemeName;
|
||||||
|
default:
|
||||||
|
return LightThemeName;
|
||||||
|
}
|
||||||
|
} else if (theme === undefined || typeof theme === "undefined") {
|
||||||
|
return LightThemeName;
|
||||||
|
} else {
|
||||||
|
const themeName = "custom-vs";
|
||||||
|
|
||||||
|
// Skip redefining the same custom theme if it is the same theme data.
|
||||||
|
if (customThemeData !== theme) {
|
||||||
|
monaco.editor.defineTheme(themeName, theme);
|
||||||
|
customThemeData = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMapStateToProps = (initialState: AppState, initialProps: IMonacoProps) => {
|
||||||
|
const { id, contentRef } = initialProps;
|
||||||
|
const mapStateToProps = (state: AppState, ownProps: IMonacoProps & IMonacoStateProps) => {
|
||||||
|
return {
|
||||||
|
language: getCellMonacoLanguage(
|
||||||
|
state,
|
||||||
|
contentRef,
|
||||||
|
id,
|
||||||
|
ownProps.cellLanguageOverride,
|
||||||
|
ownProps.notebookLanguageOverride
|
||||||
|
)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a MonacoEditor instance within the MonacoContainer div
|
||||||
|
*/
|
||||||
|
export class MonacoEditor extends React.Component<IMonacoProps & IMonacoStateProps> {
|
||||||
|
editor?: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
editorContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
contentHeight?: number;
|
||||||
|
private cursorPositionListener?: monaco.IDisposable;
|
||||||
|
|
||||||
|
constructor(props: IMonacoProps & IMonacoStateProps) {
|
||||||
|
super(props);
|
||||||
|
this.onFocus = this.onFocus.bind(this);
|
||||||
|
this.onBlur = this.onBlur.bind(this);
|
||||||
|
this.calculateHeight = this.calculateHeight.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDidChangeModelContent(e: monaco.editor.IModelContentChangedEvent): void {
|
||||||
|
if (this.editor) {
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(this.editor.getValue(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calculateHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust the height of editor
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The way to determine how many lines we should display in editor:
|
||||||
|
* If numberOfLines is not set or set to 0, we adjust the height to fit the content
|
||||||
|
* If numberOfLines is specified we respect that setting
|
||||||
|
*/
|
||||||
|
calculateHeight(): void {
|
||||||
|
// Make sure we have an editor
|
||||||
|
if (!this.editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we have a model
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||||
|
const expectedLines = this.props.numberOfLines || model.getLineCount();
|
||||||
|
// The find & replace menu takes up 2 lines, that is why 2 line is set as the minimum number of lines
|
||||||
|
// TODO: we should either disable the find/replace menu or auto expand the editor when find/replace is triggerred.
|
||||||
|
const finalizedLines = Math.max(expectedLines, 1) + 1;
|
||||||
|
const lineHeight = this.editor.getConfiguration().lineHeight;
|
||||||
|
|
||||||
|
const contentHeight = finalizedLines * lineHeight;
|
||||||
|
if (this.contentHeight !== contentHeight) {
|
||||||
|
this.editorContainerRef.current.style.height = contentHeight + "px";
|
||||||
|
this.editor.layout();
|
||||||
|
this.contentHeight = contentHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||||
|
// Register Jupyter completion provider if needed
|
||||||
|
this.registerCompletionProvider();
|
||||||
|
|
||||||
|
// Use Monaco model uri if provided. Otherwise, create a new model uri using editor id.
|
||||||
|
const uri = this.props.modelUri ? this.props.modelUri : monaco.Uri.file(this.props.id);
|
||||||
|
|
||||||
|
// Only create a new model if it does not exist. For example, when we double click on a markdown cell,
|
||||||
|
// an editor model is created for it. Once we go back to markdown preview mode that doesn't use the editor,
|
||||||
|
// double clicking on the markdown cell will again instantiate a monaco editor. In that case, we should
|
||||||
|
// rebind the previously created editor model for the markdown instead of recreating one. Monaco does not
|
||||||
|
// allow models to be recreated with the same uri.
|
||||||
|
let model = monaco.editor.getModel(uri);
|
||||||
|
if (!model) {
|
||||||
|
model = monaco.editor.createModel(this.props.value, this.props.language, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Monaco editor backed by a Monaco model.
|
||||||
|
this.editor = monaco.editor.create(this.editorContainerRef.current, {
|
||||||
|
// Following are the default settings
|
||||||
|
minimap: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
autoIndent: true,
|
||||||
|
overviewRulerLanes: 1,
|
||||||
|
scrollbar: {
|
||||||
|
useShadows: false,
|
||||||
|
verticalHasArrows: false,
|
||||||
|
horizontalHasArrows: false,
|
||||||
|
vertical: "hidden",
|
||||||
|
horizontal: "hidden",
|
||||||
|
verticalScrollbarSize: 0,
|
||||||
|
horizontalScrollbarSize: 0,
|
||||||
|
arrowSize: 30
|
||||||
|
},
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
find: {
|
||||||
|
// TODO Need this?
|
||||||
|
// addExtraSpaceOnTop: false, // pops the editor out of alignment if turned on
|
||||||
|
seedSearchStringFromSelection: true, // default is true
|
||||||
|
autoFindInSelection: false // default is false
|
||||||
|
},
|
||||||
|
// Disable highlight current line, too much visual noise with it on.
|
||||||
|
// VS Code also has it disabled for their notebook experience.
|
||||||
|
renderLineHighlight: "none",
|
||||||
|
|
||||||
|
// Allow editor pop up widgets such as context menus, signature help, hover tips to be able to be
|
||||||
|
// displayed outside of the editor. Without this, the pop up widgets can be clipped.
|
||||||
|
fixedOverflowWidgets: true,
|
||||||
|
|
||||||
|
// Apply custom settings from configuration
|
||||||
|
...this.props.options,
|
||||||
|
|
||||||
|
// Apply specific settings passed-in as direct props
|
||||||
|
model,
|
||||||
|
value: this.props.value,
|
||||||
|
language: this.props.language,
|
||||||
|
readOnly: this.props.readOnly,
|
||||||
|
lineNumbers: this.props.lineNumbers ? "on" : "off",
|
||||||
|
theme: getMonacoTheme(this.props.theme)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEditorTopMargin();
|
||||||
|
|
||||||
|
// Ignore Ctrl + Enter
|
||||||
|
// tslint:disable-next-line no-bitwise
|
||||||
|
this.editor.addCommand(
|
||||||
|
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||||
|
() => {
|
||||||
|
// Do nothing. This is handled elsewhere, we just don't want the editor to put the newline.
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
// TODO Add right context
|
||||||
|
|
||||||
|
this.toggleEditorOptions(this.props.editorFocused);
|
||||||
|
|
||||||
|
if (this.props.editorFocused) {
|
||||||
|
if (!this.editor.hasTextFocus()) {
|
||||||
|
// Bring browser focus to the editor if text not already in focus
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
this.registerCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Need to remove the event listener when the editor is disposed, or we have a memory leak here.
|
||||||
|
// The same applies to the other event listeners below
|
||||||
|
// Adds listener under the resize window event which calls the resize method
|
||||||
|
window.addEventListener("resize", this.resize.bind(this));
|
||||||
|
|
||||||
|
// Adds listeners for undo and redo actions emitted from the toolbar
|
||||||
|
this.editorContainerRef.current.addEventListener("undo", () => {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.trigger("undo-event", "undo", {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.editorContainerRef.current.addEventListener("redo", () => {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.trigger("redo-event", "redo", {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editor.onDidChangeModelContent(this.onDidChangeModelContent.bind(this));
|
||||||
|
this.editor.onDidFocusEditorText(this.onFocus);
|
||||||
|
this.editor.onDidBlurEditorText(this.onBlur);
|
||||||
|
this.calculateHeight();
|
||||||
|
|
||||||
|
// FIXME: This might need further investigation as the props value should be respected in construction
|
||||||
|
// The following is a mitigation measure till that time
|
||||||
|
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||||
|
this.editor.setValue(this.props.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEditorTopMargin(): void {
|
||||||
|
if (this.editor) {
|
||||||
|
// Monaco editor doesn't have margins
|
||||||
|
// https://github.com/notable/notable/issues/551
|
||||||
|
// This is a workaround to add an editor area 12px padding at the top
|
||||||
|
// so that cursors rendered by collab decorators could be visible without being cut.
|
||||||
|
this.editor.changeViewZones(changeAccessor => {
|
||||||
|
const domNode = document.createElement("div");
|
||||||
|
changeAccessor.addZone({
|
||||||
|
afterLineNumber: 0,
|
||||||
|
heightInPx: 12,
|
||||||
|
domNode
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells editor to check the surrounding container size and resize itself appropriately
|
||||||
|
*/
|
||||||
|
resize(): void {
|
||||||
|
if (this.editor && this.props.editorFocused) {
|
||||||
|
this.editor.layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(): void {
|
||||||
|
if (!this.editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, channels, /* language, contentRef, id,*/ editorFocused, theme } = this.props;
|
||||||
|
|
||||||
|
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||||
|
if (this.editor.getValue() !== value) {
|
||||||
|
this.editor.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
completionProvider.setChannels(channels);
|
||||||
|
|
||||||
|
// Register Jupyter completion provider if needed
|
||||||
|
this.registerCompletionProvider();
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Apply new model to the editor when the language is changed.
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (model && language && model.getModeId() !== language) {
|
||||||
|
const newUri = DocumentUri.createCellUri(contentRef, id, language);
|
||||||
|
if (!monaco.editor.getModel(newUri)) {
|
||||||
|
// Save the cursor position before we set new model.
|
||||||
|
const position = this.editor.getPosition();
|
||||||
|
|
||||||
|
// Set new model targeting the changed language.
|
||||||
|
this.editor.setModel(monaco.editor.createModel(value, language, newUri));
|
||||||
|
this.addEditorTopMargin();
|
||||||
|
|
||||||
|
// Restore cursor position to new model.
|
||||||
|
if (position) {
|
||||||
|
this.editor.setPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose of the old model in a seperate event. We cannot dispose of the model within the
|
||||||
|
// componentDidUpdate method or else the editor will throw an exception. Zero in the timeout field
|
||||||
|
// means execute immediately but in a seperate next event.
|
||||||
|
setTimeout(() => model.dispose(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (theme) {
|
||||||
|
monaco.editor.setTheme(getMonacoTheme(theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the multi-tabs scenario, when the notebook is hidden by setting "display:none",
|
||||||
|
// Any state update propagated here would cause a UI re-layout, monaco-editor will then recalculate
|
||||||
|
// and set its height to 5px.
|
||||||
|
// To work around that issue, we skip updating the UI when parent element's offsetParent is null (which
|
||||||
|
// indicate an ancient element is hidden by display set to none)
|
||||||
|
// We may revisit this when we get to refactor for multi-notebooks.
|
||||||
|
if (!this.editorContainerRef.current?.offsetParent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set focus
|
||||||
|
if (editorFocused && !this.editor.hasTextFocus()) {
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tells the editor pane to check if its container has changed size and fill appropriately
|
||||||
|
this.editor.layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if (this.editor) {
|
||||||
|
try {
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (model) {
|
||||||
|
model.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.dispose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error occurs in disposing editor: ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="monaco-container">
|
||||||
|
<div ref={this.editorContainerRef} id={`editor-${this.props.id}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register default kernel-based completion provider.
|
||||||
|
* @param language Language
|
||||||
|
*/
|
||||||
|
registerDefaultCompletionProvider(language: string): void {
|
||||||
|
// onLanguage event is emitted only once per language when language is first time needed.
|
||||||
|
monaco.languages.onLanguage(language, () => {
|
||||||
|
monaco.languages.registerCompletionItemProvider(language, completionProvider);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFocus() {
|
||||||
|
this.props.onFocusChange(true);
|
||||||
|
this.toggleEditorOptions(true);
|
||||||
|
this.registerCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBlur() {
|
||||||
|
this.props.onFocusChange(false);
|
||||||
|
this.toggleEditorOptions(false);
|
||||||
|
this.unregisterCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerCursorListener() {
|
||||||
|
if (this.editor && this.props.onCursorPositionChange) {
|
||||||
|
const selection = this.editor.getSelection();
|
||||||
|
this.props.onCursorPositionChange(selection);
|
||||||
|
|
||||||
|
if (!this.cursorPositionListener) {
|
||||||
|
this.cursorPositionListener = this.editor.onDidChangeCursorSelection(event =>
|
||||||
|
this.props.onCursorPositionChange!(event.selection)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unregisterCursorListener() {
|
||||||
|
if (this.cursorPositionListener) {
|
||||||
|
this.cursorPositionListener.dispose();
|
||||||
|
this.cursorPositionListener = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle editor options based on if the editor is in active state (i.e. focused).
|
||||||
|
* When the editor is not active, we want to deactivate some of the visual noise.
|
||||||
|
* @param isActive Whether editor is active.
|
||||||
|
*/
|
||||||
|
private toggleEditorOptions(isActive: boolean) {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.updateOptions({
|
||||||
|
matchBrackets: isActive,
|
||||||
|
occurrencesHighlight: isActive,
|
||||||
|
renderIndentGuides: isActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register language features for target language. Call before setting language type to model.
|
||||||
|
*/
|
||||||
|
private registerCompletionProvider() {
|
||||||
|
const { enableCompletion, language, onRegisterCompletionProvider, shouldRegisterDefaultCompletion } = this.props;
|
||||||
|
|
||||||
|
if (enableCompletion && language) {
|
||||||
|
if (onRegisterCompletionProvider) {
|
||||||
|
onRegisterCompletionProvider(language);
|
||||||
|
} else if (shouldRegisterDefaultCompletion) {
|
||||||
|
this.registerDefaultCompletionProvider(language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect<IMonacoStateProps, void, IMonacoProps, AppState>(makeMapStateToProps)(MonacoEditor);
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
// import * as monaco from "../monaco";
|
||||||
|
import { Observable, Observer } from "rxjs";
|
||||||
|
import { first, map } from "rxjs/operators";
|
||||||
|
import { childOf, JupyterMessage, ofMessageType, Channels } from "@nteract/messaging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: import from nteract when the changes under editor-base.ts are ported to nteract.
|
||||||
|
*/
|
||||||
|
import { CompletionResults, CompletionMatch, completionRequest, js_idx_to_char_idx } from "../editor-base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter to Monaco completion item kinds.
|
||||||
|
*/
|
||||||
|
const unknownJupyterKind = "<unknown>";
|
||||||
|
const jupyterToMonacoCompletionItemKind = {
|
||||||
|
[unknownJupyterKind]: monaco.languages.CompletionItemKind.Field,
|
||||||
|
class: monaco.languages.CompletionItemKind.Class,
|
||||||
|
function: monaco.languages.CompletionItemKind.Function,
|
||||||
|
keyword: monaco.languages.CompletionItemKind.Keyword,
|
||||||
|
instance: monaco.languages.CompletionItemKind.Variable,
|
||||||
|
statement: monaco.languages.CompletionItemKind.Variable
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completion item provider.
|
||||||
|
*/
|
||||||
|
class CompletionItemProvider implements monaco.languages.CompletionItemProvider {
|
||||||
|
private channels: Channels | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Channels of Jupyter kernel.
|
||||||
|
* @param channels Channels of Jupyter kernel.
|
||||||
|
*/
|
||||||
|
setChannels(channels: Channels | undefined) {
|
||||||
|
this.channels = channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether provider is connected to Jupyter kernel.
|
||||||
|
*/
|
||||||
|
get isConnectedToKernel() {
|
||||||
|
return !!this.channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional characters to trigger completion other than Ctrl+Space.
|
||||||
|
*/
|
||||||
|
get triggerCharacters() {
|
||||||
|
return [" ", "<", "/", ".", "="];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of completion items at position of cursor.
|
||||||
|
* @param model Monaco editor text model.
|
||||||
|
* @param position Position of cursor.
|
||||||
|
*/
|
||||||
|
async provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
||||||
|
// Convert to zero-based index
|
||||||
|
let cursorPos = model.getOffsetAt(position);
|
||||||
|
const code = model.getValue();
|
||||||
|
cursorPos = js_idx_to_char_idx(cursorPos, code);
|
||||||
|
|
||||||
|
// Get completions from Jupyter kernel if its Channels is connected
|
||||||
|
let items = [];
|
||||||
|
if (this.channels) {
|
||||||
|
try {
|
||||||
|
const message = completionRequest(code, cursorPos);
|
||||||
|
items = await this.codeCompleteObservable(this.channels, message, model).toPromise();
|
||||||
|
} catch (error) {
|
||||||
|
// Temporary log error to console until we settle on how we log in V3
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve<monaco.languages.CompletionList>({
|
||||||
|
suggestions: items,
|
||||||
|
incomplete: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of completion items from Jupyter kernel.
|
||||||
|
* @param channels Channels of Jupyter kernel.
|
||||||
|
* @param message Jupyter message for completion request.
|
||||||
|
* @param model Text model.
|
||||||
|
*/
|
||||||
|
private codeCompleteObservable(channels: Channels, message: JupyterMessage, model: monaco.editor.ITextModel) {
|
||||||
|
// Process completion response
|
||||||
|
const completion$ = channels.pipe(
|
||||||
|
childOf(message),
|
||||||
|
ofMessageType("complete_reply"),
|
||||||
|
map(entry => entry.content),
|
||||||
|
first(),
|
||||||
|
map(results => this.adaptToMonacoCompletions(results, model))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe and send completion request message
|
||||||
|
return Observable.create((observer: Observer<unknown>) => {
|
||||||
|
const subscription = completion$.subscribe(observer);
|
||||||
|
channels.next(message);
|
||||||
|
return subscription;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Jupyter completion result to list of Monaco completion items.
|
||||||
|
*/
|
||||||
|
private adaptToMonacoCompletions(results: CompletionResults, model: monaco.editor.ITextModel) {
|
||||||
|
let range: monaco.IRange;
|
||||||
|
let percentCount = 0;
|
||||||
|
let matches = results ? results.matches : [];
|
||||||
|
if (results.metadata && results.metadata._jupyter_types_experimental) {
|
||||||
|
matches = results.metadata._jupyter_types_experimental as CompletionMatch[];
|
||||||
|
}
|
||||||
|
return matches.map((match: CompletionMatch, index: number) => {
|
||||||
|
if (typeof match === "string") {
|
||||||
|
const text = this.sanitizeText(match);
|
||||||
|
const filtered = this.getFilterText(text);
|
||||||
|
return {
|
||||||
|
kind: this.adaptToMonacoCompletionItemKind(unknownJupyterKind),
|
||||||
|
label: text,
|
||||||
|
insertText: text,
|
||||||
|
filterText: filtered,
|
||||||
|
sortText: this.getSortText(index)
|
||||||
|
} as monaco.languages.CompletionItem;
|
||||||
|
} else {
|
||||||
|
// We only need to get the range once as the range is the same for all completion items in the list.
|
||||||
|
if (!range) {
|
||||||
|
const start = model.getPositionAt(match.start);
|
||||||
|
const end = model.getPositionAt(match.end);
|
||||||
|
range = {
|
||||||
|
startLineNumber: start.lineNumber,
|
||||||
|
startColumn: start.column,
|
||||||
|
endLineNumber: end.lineNumber,
|
||||||
|
endColumn: end.column
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the range representing the text before the completion action was invoked.
|
||||||
|
// If the text starts with magics % indicator, we need to track how many of these indicators exist
|
||||||
|
// so that we ensure the insertion text only inserts the delta between what the user typed versus
|
||||||
|
// what is recommended by the completion. Without this, there will be extra % insertions.
|
||||||
|
// Example:
|
||||||
|
// User types %%p then suggestion list will recommend %%python, if we now commit the item then the
|
||||||
|
// final text in the editor becomes %%p%%python instead of %%python. This is why the tracking code
|
||||||
|
// below is needed. This behavior is only specific to the magics % indicators as Monaco does not
|
||||||
|
// handle % characters in their completion list well.
|
||||||
|
const rangeText = model.getValueInRange(range);
|
||||||
|
if (rangeText.startsWith("%%")) {
|
||||||
|
percentCount = 2;
|
||||||
|
} else if (rangeText.startsWith("%")) {
|
||||||
|
percentCount = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.sanitizeText(match.text);
|
||||||
|
const filtered = this.getFilterText(text);
|
||||||
|
const insert = this.getInsertText(text, percentCount);
|
||||||
|
return {
|
||||||
|
kind: this.adaptToMonacoCompletionItemKind(match.type as keyof typeof jupyterToMonacoCompletionItemKind),
|
||||||
|
label: text,
|
||||||
|
insertText: percentCount > 0 ? insert : text,
|
||||||
|
filterText: filtered,
|
||||||
|
sortText: this.getSortText(index)
|
||||||
|
} as monaco.languages.CompletionItem;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Jupyter completion item kind to Monaco completion item kind.
|
||||||
|
* @param kind Jupyter completion item kind.
|
||||||
|
*/
|
||||||
|
private adaptToMonacoCompletionItemKind(kind: keyof typeof jupyterToMonacoCompletionItemKind) {
|
||||||
|
const result = jupyterToMonacoCompletionItemKind[kind];
|
||||||
|
return result ? result : jupyterToMonacoCompletionItemKind[unknownJupyterKind];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove everything before a dot. Jupyter completion results like to include all characters before
|
||||||
|
* the trigger character. For example, if user types "myarray.", we expect the completion results to
|
||||||
|
* show "append", "pop", etc. but for the actual case, it will show "myarray.append", "myarray.pop",
|
||||||
|
* etc. so we are going to sanitize the text.
|
||||||
|
* @param text Text of Jupyter completion item
|
||||||
|
*/
|
||||||
|
private sanitizeText(text: string) {
|
||||||
|
const index = text.lastIndexOf(".");
|
||||||
|
return index > -1 && index < text.length - 1 ? text.substring(index + 1) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove magics all % characters as Monaco doesn't like them for the filtering text.
|
||||||
|
* Without this, completion won't show magics match items.
|
||||||
|
* @param text Text of Jupyter completion item.
|
||||||
|
*/
|
||||||
|
private getFilterText(text: string) {
|
||||||
|
return text.replace(/%/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get insertion text handling what to insert for the magics case depending on what
|
||||||
|
* has already been typed.
|
||||||
|
* @param text Text of Jupyter completion item.
|
||||||
|
* @param percentCount Number of percent characters to remove
|
||||||
|
*/
|
||||||
|
private getInsertText(text: string, percentCount: number) {
|
||||||
|
for (let i = 0; i < percentCount; i++) {
|
||||||
|
text = text.replace("%", "");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps numbers to strings, such that if a>b numerically, f(a)>f(b) lexicograhically.
|
||||||
|
* 1 -> "za", 26 -> "zz", 27 -> "zza", 28 -> "zzb", 52 -> "zzz", 53 ->"zzza"
|
||||||
|
* @param order Number to be converted to a sorting-string. order >= 0.
|
||||||
|
* @returns A string representing the order.
|
||||||
|
*/
|
||||||
|
private getSortText(order: number): string {
|
||||||
|
order++;
|
||||||
|
const numCharacters = 26; // "z" - "a" + 1;
|
||||||
|
const div = Math.floor(order / numCharacters);
|
||||||
|
|
||||||
|
let sortText = "z";
|
||||||
|
for (let i = 0; i < div; i++) {
|
||||||
|
sortText += "z";
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainder = order % numCharacters;
|
||||||
|
if (remainder > 0) {
|
||||||
|
sortText += String.fromCharCode(96 + remainder);
|
||||||
|
}
|
||||||
|
return sortText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionProvider = new CompletionItemProvider();
|
||||||
|
export { completionProvider };
|
||||||
44
src/Explorer/Notebook/MonacoEditor/converter.ts
Normal file
44
src/Explorer/Notebook/MonacoEditor/converter.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Immutable from "immutable";
|
||||||
|
import * as monaco from "./monaco";
|
||||||
|
/**
|
||||||
|
* Code Mirror to Monaco constants.
|
||||||
|
*/
|
||||||
|
export enum Mode {
|
||||||
|
markdown = "markdown",
|
||||||
|
raw = "plaintext",
|
||||||
|
python = "python",
|
||||||
|
csharp = "csharp"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Code Mirror mode to a valid Monaco Editor supported langauge
|
||||||
|
* defaults to plaintext if map not found.
|
||||||
|
* @param mode Code Mirror mode
|
||||||
|
* @returns Monaco language
|
||||||
|
*/
|
||||||
|
export function mapCodeMirrorModeToMonaco(mode: string | { name: string }): string {
|
||||||
|
let language = "";
|
||||||
|
|
||||||
|
// Parse codemirror mode object
|
||||||
|
if (typeof mode === "string") {
|
||||||
|
language = mode;
|
||||||
|
}
|
||||||
|
// Vanilla object
|
||||||
|
else if (typeof mode === "object" && mode.name) {
|
||||||
|
language = mode.name;
|
||||||
|
}
|
||||||
|
// Immutable Map
|
||||||
|
else if (Immutable.Map.isMap(mode) && mode.has("name")) {
|
||||||
|
language = mode.get("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to handle "ipython" as a special case since it is not a registered language
|
||||||
|
if (language === "ipython") {
|
||||||
|
return Mode.python;
|
||||||
|
} else if (language === "text/x-csharp") {
|
||||||
|
return Mode.csharp;
|
||||||
|
} else if (monaco.languages.getEncodedLanguageId(language) > 0) {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
return Mode.raw;
|
||||||
|
}
|
||||||
76
src/Explorer/Notebook/MonacoEditor/editor-base.ts
Normal file
76
src/Explorer/Notebook/MonacoEditor/editor-base.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Disable linting on file since we will be moving the code below to nteract which have different rules configured.
|
||||||
|
// tslint:disable:variable-name
|
||||||
|
// tslint:disable:interface-name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Create new editor-base package in nteract repo and move all code below to new package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createMessage } from "@nteract/messaging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter messaging protocol's _jupyter_types_experimental completion result.
|
||||||
|
*/
|
||||||
|
interface CompletionResult {
|
||||||
|
end: number;
|
||||||
|
start: number;
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
displayText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Juptyer completion match item.
|
||||||
|
*/
|
||||||
|
export type CompletionMatch = string | CompletionResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter messaging protocol's complete_reply response.
|
||||||
|
*/
|
||||||
|
export interface CompletionResults {
|
||||||
|
status: string;
|
||||||
|
cursor_start: number;
|
||||||
|
cursor_end: number;
|
||||||
|
matches: CompletionMatch[];
|
||||||
|
metadata?: {
|
||||||
|
_jupyter_types_experimental?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Jupyter messaging protocol's complete_request message.
|
||||||
|
* @param code Code of editor.
|
||||||
|
* @param cursorPos cursor position represented in the Jupyter messaging protocol (character position)
|
||||||
|
*/
|
||||||
|
export const completionRequest = (code: string, cursorPos: number) =>
|
||||||
|
createMessage("complete_request", {
|
||||||
|
content: {
|
||||||
|
code,
|
||||||
|
cursor_pos: cursorPos
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript stores text as utf16 and string indices use "code units",
|
||||||
|
* which stores high-codepoint characters as "surrogate pairs",
|
||||||
|
* which occupy two indices in the JavaScript string.
|
||||||
|
* We need to translate cursor_pos in the protocol (in characters)
|
||||||
|
* to js offset (with surrogate pairs taking two spots).
|
||||||
|
* @param js_idx JavaScript index
|
||||||
|
* @param text Text
|
||||||
|
*/
|
||||||
|
export const js_idx_to_char_idx: (js_idx: number, text: string) => number = (js_idx: number, text: string): number => {
|
||||||
|
let char_idx: number = js_idx;
|
||||||
|
for (let i = 0; i + 1 < text.length && i < js_idx; i++) {
|
||||||
|
const char_code: number = text.charCodeAt(i);
|
||||||
|
// check for surrogate pair
|
||||||
|
if (char_code >= 0xd800 && char_code <= 0xdbff) {
|
||||||
|
const next_char_code: number = text.charCodeAt(i + 1);
|
||||||
|
if (next_char_code >= 0xdc00 && next_char_code <= 0xdfff) {
|
||||||
|
char_idx--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return char_idx;
|
||||||
|
};
|
||||||
10
src/Explorer/Notebook/MonacoEditor/monaco.ts
Normal file
10
src/Explorer/Notebook/MonacoEditor/monaco.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export * from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Set the custom worker url to workaround the cross-domain issue with creating web worker
|
||||||
|
// * See https://github.com/microsoft/monaco-editor/blob/master/docs/integrate-amd-cross.md for more details
|
||||||
|
// * This step has to be executed after a importing of monaco-editor once per chunk to make sure
|
||||||
|
// * the custom worker url overwrites the one from monaco-editor module itself.
|
||||||
|
// */
|
||||||
|
// import { setMonacoWorkerUrl } from "./workerUrl";
|
||||||
|
// setMonacoWorkerUrl();
|
||||||
67
src/Explorer/Notebook/MonacoEditor/selectors.ts
Normal file
67
src/Explorer/Notebook/MonacoEditor/selectors.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { AppState, ContentRef, selectors as nteractSelectors } from "@nteract/core";
|
||||||
|
import { CellId } from "@nteract/commutable";
|
||||||
|
import { Mode, mapCodeMirrorModeToMonaco } from "./converter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given cell, falling back to the notebook language if one for the cell is not defined.
|
||||||
|
*/
|
||||||
|
export const getCellMonacoLanguage = (
|
||||||
|
state: AppState,
|
||||||
|
contentRef: ContentRef,
|
||||||
|
cellId: CellId,
|
||||||
|
cellLanguageOverride?: string,
|
||||||
|
notebookLanguageOverride?: string
|
||||||
|
): string => {
|
||||||
|
const model = nteractSelectors.model(state, { contentRef });
|
||||||
|
if (!model || model.type !== "notebook") {
|
||||||
|
throw new Error("Connected Editor components should not be used with non-notebook models");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = nteractSelectors.notebook.cellById(model, { id: cellId });
|
||||||
|
if (!cell) {
|
||||||
|
throw new Error("Invalid cell id");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cell.cell_type) {
|
||||||
|
case "markdown":
|
||||||
|
return Mode.markdown;
|
||||||
|
case "raw":
|
||||||
|
return Mode.raw;
|
||||||
|
case "code":
|
||||||
|
if (cellLanguageOverride) {
|
||||||
|
return mapCodeMirrorModeToMonaco(cellLanguageOverride);
|
||||||
|
} else {
|
||||||
|
// Fall back to notebook language if cell language isn't present.
|
||||||
|
return getNotebookMonacoLanguage(state, contentRef, notebookLanguageOverride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given notebook.
|
||||||
|
*/
|
||||||
|
export const getNotebookMonacoLanguage = (
|
||||||
|
state: AppState,
|
||||||
|
contentRef: ContentRef,
|
||||||
|
notebookLanguageOverride?: string
|
||||||
|
): string => {
|
||||||
|
const model = nteractSelectors.model(state, { contentRef });
|
||||||
|
if (!model || model.type !== "notebook") {
|
||||||
|
throw new Error("Connected Editor components should not be used with non-notebook models");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notebookLanguageOverride) {
|
||||||
|
return mapCodeMirrorModeToMonaco(notebookLanguageOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kernelRef = model.kernelRef;
|
||||||
|
let codeMirrorMode;
|
||||||
|
// Try to get the CodeMirror mode from the kernel.
|
||||||
|
if (kernelRef) {
|
||||||
|
codeMirrorMode = nteractSelectors.kernel(state, { kernelRef })?.info?.codemirrorMode;
|
||||||
|
}
|
||||||
|
// As a fallback, get the CodeMirror mode from the notebook itself.
|
||||||
|
codeMirrorMode = codeMirrorMode ?? nteractSelectors.notebook.codeMirrorMode(model);
|
||||||
|
|
||||||
|
return mapCodeMirrorModeToMonaco(codeMirrorMode);
|
||||||
|
};
|
||||||
22
src/Explorer/Notebook/MonacoEditor/styles.css
Normal file
22
src/Explorer/Notebook/MonacoEditor/styles.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
For the following components, we use the inherited width values from monaco-container.
|
||||||
|
On resizing the browser, the width of monaco-container will be calculated
|
||||||
|
and we just use the calculated width for the following components
|
||||||
|
So we don't need to use Monaco editor's layout() function which is expensive operation and causes performance issues on resizing.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
TODO: These styles below are added for resizing perf improvement.
|
||||||
|
Once the virtualization is implemented, we will revisit this later.
|
||||||
|
*/
|
||||||
|
.monaco-container .monaco-editor {
|
||||||
|
width: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-container .monaco-editor .overflow-guard {
|
||||||
|
width: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26px is the left margin for .monaco-scrollable-element */
|
||||||
|
.monaco-container .monaco-editor .monaco-scrollable-element.editor-scrollable.vs {
|
||||||
|
width: calc(100% - 26px) !important;
|
||||||
|
}
|
||||||
75
src/Explorer/Notebook/MonacoEditor/theme.ts
Normal file
75
src/Explorer/Notebook/MonacoEditor/theme.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import * as monaco from "./monaco";
|
||||||
|
|
||||||
|
// TODO: move defineTheme calls to an initialization function
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default light theme with customized background
|
||||||
|
*/
|
||||||
|
export const LightThemeName = "vs-light";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for light theme
|
||||||
|
*/
|
||||||
|
export const customMonacoLightTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs", // Derive from default light theme of Monaco
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
// We want Monaco background to use the same background for our themes.
|
||||||
|
// Without this, the Monaco light theme has a yellowish tone.
|
||||||
|
// Verified with UX that white meets all the accessbility requirements for light
|
||||||
|
// and high contrast light theme.
|
||||||
|
"editor.background": "#FFFFFF"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(LightThemeName, customMonacoLightTheme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default dark theme with customized background
|
||||||
|
*/
|
||||||
|
export const DarkThemeName = "aznb-dark";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for dark theme
|
||||||
|
*/
|
||||||
|
export const customMonacoDarkTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs-dark", // Derive from default dark theme of Monaco
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
"editor.background": "#1b1a19"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(DarkThemeName, customMonacoDarkTheme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The custom high contrast light theme with customized background
|
||||||
|
*/
|
||||||
|
export const HCLightThemeName = "hc-light";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for light high contrast mode
|
||||||
|
*/
|
||||||
|
export const customMonacoHCLightTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs", // Derive from default light theme of Monaco; change all grey colors to black to comply with highcontrast rules
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: "annotation", foreground: "000000" },
|
||||||
|
{ token: "delimiter.html", foreground: "000000" },
|
||||||
|
{ token: "operator.scss", foreground: "000000" },
|
||||||
|
{ token: "operator.sql", foreground: "000000" },
|
||||||
|
{ token: "operator.swift", foreground: "000000" },
|
||||||
|
{ token: "predefined.sql", foreground: "000000" }
|
||||||
|
],
|
||||||
|
colors: {
|
||||||
|
// We want Monaco background to use the same background for our themes.
|
||||||
|
// Without this, the Monaco light theme has a yellowish tone.
|
||||||
|
// Verified with UX that white meets all the accessbility requirements for light
|
||||||
|
// and high contrast light theme.
|
||||||
|
"editor.background": "#FFFFFF"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(HCLightThemeName, customMonacoHCLightTheme);
|
||||||
@@ -98,7 +98,7 @@ export class NotebookComponentBootstrapper {
|
|||||||
actions.fetchContentFulfilled({
|
actions.fetchContentFulfilled({
|
||||||
filepath: undefined,
|
filepath: undefined,
|
||||||
model: NotebookComponentBootstrapper.wrapModelIntoContent(name, undefined, content),
|
model: NotebookComponentBootstrapper.wrapModelIntoContent(name, undefined, content),
|
||||||
kernelRef: createKernelRef(),
|
kernelRef: undefined, // must be undefined or it will be auto-started by the epic
|
||||||
contentRef: this.contentRef
|
contentRef: this.contentRef
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import * as Immutable from "immutable";
|
import * as Immutable from "immutable";
|
||||||
import { ActionsObservable, StateObservable } from "redux-observable";
|
import { ActionsObservable, StateObservable } from "redux-observable";
|
||||||
import { Subject } from "rxjs";
|
import { Subject, empty } from "rxjs";
|
||||||
import { toArray } from "rxjs/operators";
|
import { toArray } from "rxjs/operators";
|
||||||
import { makeNotebookRecord } from "@nteract/commutable";
|
import { makeNotebookRecord } from "@nteract/commutable";
|
||||||
import { actions, state } from "@nteract/core";
|
import { actions, state } from "@nteract/core";
|
||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
|
|
||||||
import { CdbAppState, makeCdbRecord } from "./types";
|
import { CdbAppState, makeCdbRecord } from "./types";
|
||||||
import { launchWebSocketKernelEpic } from "./epics";
|
import { launchWebSocketKernelEpic, autoStartKernelEpic } from "./epics";
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
import { NotebookUtil } from "../NotebookUtil";
|
||||||
|
|
||||||
import { sessions } from "rx-jupyter";
|
import { sessions } from "rx-jupyter";
|
||||||
@@ -74,46 +74,47 @@ describe("Extract kernel from notebook", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
app: state.makeAppRecord({
|
||||||
|
host: state.makeJupyterHostRecord({
|
||||||
|
type: "jupyter",
|
||||||
|
token: "eh",
|
||||||
|
basePath: "/"
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
comms: state.makeCommsRecord(),
|
||||||
|
config: Immutable.Map({}),
|
||||||
|
core: state.makeStateRecord({
|
||||||
|
kernelRef: "fake",
|
||||||
|
entities: state.makeEntitiesRecord({
|
||||||
|
contents: state.makeContentsRecord({
|
||||||
|
byRef: Immutable.Map({
|
||||||
|
fakeContentRef: state.makeNotebookContentRecord()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
kernels: state.makeKernelsRecord({
|
||||||
|
byRef: Immutable.Map({
|
||||||
|
fake: state.makeRemoteKernelRecord({
|
||||||
|
type: "websocket",
|
||||||
|
channels: new Subject<any>(),
|
||||||
|
kernelSpecName: "fancy",
|
||||||
|
id: "0"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
cdb: makeCdbRecord({
|
||||||
|
databaseAccountName: "dbAccountName",
|
||||||
|
defaultExperience: "defaultExperience"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
describe("launchWebSocketKernelEpic", () => {
|
describe("launchWebSocketKernelEpic", () => {
|
||||||
const createSpy = sinon.spy(sessions, "create");
|
const createSpy = sinon.spy(sessions, "create");
|
||||||
|
|
||||||
const contentRef = "fakeContentRef";
|
const contentRef = "fakeContentRef";
|
||||||
const kernelRef = "fake";
|
const kernelRef = "fake";
|
||||||
const initialState = {
|
|
||||||
app: state.makeAppRecord({
|
|
||||||
host: state.makeJupyterHostRecord({
|
|
||||||
type: "jupyter",
|
|
||||||
token: "eh",
|
|
||||||
basePath: "/"
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
comms: state.makeCommsRecord(),
|
|
||||||
config: Immutable.Map({}),
|
|
||||||
core: state.makeStateRecord({
|
|
||||||
kernelRef: "fake",
|
|
||||||
entities: state.makeEntitiesRecord({
|
|
||||||
contents: state.makeContentsRecord({
|
|
||||||
byRef: Immutable.Map({
|
|
||||||
fakeContentRef: state.makeNotebookContentRecord()
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
kernels: state.makeKernelsRecord({
|
|
||||||
byRef: Immutable.Map({
|
|
||||||
fake: state.makeRemoteKernelRecord({
|
|
||||||
type: "websocket",
|
|
||||||
channels: new Subject<any>(),
|
|
||||||
kernelSpecName: "fancy",
|
|
||||||
id: "0"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
cdb: makeCdbRecord({
|
|
||||||
databaseAccountName: "dbAccountName",
|
|
||||||
defaultExperience: "defaultExperience"
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
it("launches remote kernels", async () => {
|
it("launches remote kernels", async () => {
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
|
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
|
||||||
@@ -490,3 +491,55 @@ 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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 { webSocket } from "rxjs/webSocket";
|
||||||
import { ActionsObservable, StateObservable } from "redux-observable";
|
import { ActionsObservable, StateObservable } from "redux-observable";
|
||||||
import { ofType } from "redux-observable";
|
import { ofType } from "redux-observable";
|
||||||
@@ -77,7 +77,7 @@ const addInitialCodeCellEpic = (
|
|||||||
|
|
||||||
// If it's not a notebook, we shouldn't be here
|
// If it's not a notebook, we shouldn't be here
|
||||||
if (!model || model.type !== "notebook") {
|
if (!model || model.type !== "notebook") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(model);
|
const cellOrder = selectors.notebook.cellOrder(model);
|
||||||
@@ -90,7 +90,40 @@ const addInitialCodeCellEpic = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return empty();
|
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"
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -288,7 +321,7 @@ export const launchWebSocketKernelEpic = (
|
|||||||
const state = state$.value;
|
const state = state$.value;
|
||||||
const host = selectors.currentHost(state);
|
const host = selectors.currentHost(state);
|
||||||
if (host.type !== "jupyter") {
|
if (host.type !== "jupyter") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
|
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
|
||||||
serverConfig.userPuid = getUserPuid();
|
serverConfig.userPuid = getUserPuid();
|
||||||
@@ -299,7 +332,7 @@ export const launchWebSocketKernelEpic = (
|
|||||||
|
|
||||||
const content = selectors.content(state, { contentRef });
|
const content = selectors.content(state, { contentRef });
|
||||||
if (!content || content.type !== "notebook") {
|
if (!content || content.type !== "notebook") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
let kernelSpecToLaunch = kernelSpecName;
|
let kernelSpecToLaunch = kernelSpecName;
|
||||||
@@ -513,26 +546,26 @@ const changeWebSocketKernelEpic = (
|
|||||||
const state = state$.value;
|
const state = state$.value;
|
||||||
const host = selectors.currentHost(state);
|
const host = selectors.currentHost(state);
|
||||||
if (host.type !== "jupyter") {
|
if (host.type !== "jupyter") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
|
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
|
||||||
if (!oldKernelRef) {
|
if (!oldKernelRef) {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldKernel = selectors.kernel(state, { kernelRef: oldKernelRef });
|
const oldKernel = selectors.kernel(state, { kernelRef: oldKernelRef });
|
||||||
if (!oldKernel || oldKernel.type !== "websocket") {
|
if (!oldKernel || oldKernel.type !== "websocket") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
const { sessionId } = oldKernel;
|
const { sessionId } = oldKernel;
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = selectors.content(state, { contentRef });
|
const content = selectors.content(state, { contentRef });
|
||||||
if (!content || content.type !== "notebook") {
|
if (!content || content.type !== "notebook") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
filepath,
|
filepath,
|
||||||
@@ -593,7 +626,7 @@ const focusInitialCodeCellEpic = (
|
|||||||
|
|
||||||
// If it's not a notebook, we shouldn't be here
|
// If it's not a notebook, we shouldn't be here
|
||||||
if (!model || model.type !== "notebook") {
|
if (!model || model.type !== "notebook") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(model);
|
const cellOrder = selectors.notebook.cellOrder(model);
|
||||||
@@ -608,7 +641,7 @@ const focusInitialCodeCellEpic = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return empty();
|
return EMPTY;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -661,7 +694,7 @@ const notificationsToUserEpic = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return empty();
|
return EMPTY;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -701,7 +734,7 @@ const handleKernelConnectionLostEpic = (
|
|||||||
if (explorer) {
|
if (explorer) {
|
||||||
explorer.showOkModalDialog("kernel restarts", msg);
|
explorer.showOkModalDialog("kernel restarts", msg);
|
||||||
}
|
}
|
||||||
return of(empty());
|
return of(EMPTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
return concat(
|
return concat(
|
||||||
@@ -814,7 +847,7 @@ const closeUnsupportedMimetypesEpic = (
|
|||||||
explorer.showOkModalDialog("File cannot be rendered", msg);
|
explorer.showOkModalDialog("File cannot be rendered", msg);
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||||
}
|
}
|
||||||
return empty();
|
return EMPTY;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -842,13 +875,14 @@ const closeContentFailedToFetchEpic = (
|
|||||||
explorer.showOkModalDialog("Failure to load", msg);
|
explorer.showOkModalDialog("Failure to load", msg);
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||||
}
|
}
|
||||||
return empty();
|
return EMPTY;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const allEpics = [
|
export const allEpics = [
|
||||||
addInitialCodeCellEpic,
|
addInitialCodeCellEpic,
|
||||||
|
autoStartKernelEpic,
|
||||||
focusInitialCodeCellEpic,
|
focusInitialCodeCellEpic,
|
||||||
notificationsToUserEpic,
|
notificationsToUserEpic,
|
||||||
launchWebSocketKernelEpic,
|
launchWebSocketKernelEpic,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export class NotebookContentClient {
|
|||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
throw new Error(`Parent must be a directory: ${parent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = `${parent.path}/${name}`;
|
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
||||||
if (await this.checkIfFilepathExists(filepath)) {
|
if (await this.checkIfFilepathExists(filepath)) {
|
||||||
throw new Error(`File already exists: ${filepath}`);
|
throw new Error(`File already exists: ${filepath}`);
|
||||||
}
|
}
|
||||||
@@ -116,12 +116,7 @@ export class NotebookContentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
||||||
const basename = filepath.split("/").pop();
|
const parentDirPath = NotebookUtil.getParentPath(filepath);
|
||||||
let parentDirPath = filepath
|
|
||||||
.split(basename)
|
|
||||||
.shift()
|
|
||||||
.replace(/\/$/, ""); // no trailling slash
|
|
||||||
|
|
||||||
const items = await this.fetchNotebookFiles(parentDirPath);
|
const items = await this.fetchNotebookFiles(parentDirPath);
|
||||||
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { getFullName } from "../../Utils/UserUtils";
|
|||||||
import { ImmutableNotebook } from "@nteract/commutable";
|
import { ImmutableNotebook } from "@nteract/commutable";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
||||||
|
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
|
||||||
|
|
||||||
export interface NotebookManagerOptions {
|
export interface NotebookManagerOptions {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
@@ -49,6 +50,7 @@ export default class NotebookManager {
|
|||||||
|
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||||
|
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
|
||||||
|
|
||||||
public initialize(params: NotebookManagerOptions): void {
|
public initialize(params: NotebookManagerOptions): void {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
@@ -90,6 +92,12 @@ export default class NotebookManager {
|
|||||||
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
|
||||||
|
this.params.container,
|
||||||
|
this.junoClient,
|
||||||
|
this.gitHubOAuthService
|
||||||
|
);
|
||||||
|
|
||||||
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
||||||
this.gitHubClient.setToken(token?.access_token);
|
this.gitHubClient.setToken(token?.access_token);
|
||||||
|
|
||||||
@@ -108,12 +116,29 @@ export default class NotebookManager {
|
|||||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openPublishNotebookPane(
|
public refreshPinnedRepos(): void {
|
||||||
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async openPublishNotebookPane(
|
||||||
name: string,
|
name: string,
|
||||||
content: string | ImmutableNotebook,
|
content: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement
|
parentDomElement: HTMLElement,
|
||||||
): void {
|
isCodeOfConductEnabled: boolean,
|
||||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
|
isLinkInjectionEnabled: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
await this.publishNotebookPaneAdapter.open(
|
||||||
|
name,
|
||||||
|
getFullName(),
|
||||||
|
content,
|
||||||
|
parentDomElement,
|
||||||
|
isCodeOfConductEnabled,
|
||||||
|
isLinkInjectionEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public openCopyNotebookPane(name: string, content: string): void {
|
||||||
|
this.copyNotebookPaneAdapter.open(name, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Octokit's error handler uses any
|
// Octokit's error handler uses any
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { CellType } from "@nteract/commutable/src";
|
|||||||
import "./NotebookRenderer.less";
|
import "./NotebookRenderer.less";
|
||||||
import HoverableCell from "./decorators/HoverableCell";
|
import HoverableCell from "./decorators/HoverableCell";
|
||||||
import CellLabeler from "./decorators/CellLabeler";
|
import CellLabeler from "./decorators/CellLabeler";
|
||||||
|
import MonacoEditor from "../MonacoEditor/MonacoEditor";
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
|
|
||||||
export interface NotebookRendererBaseProps {
|
export interface NotebookRendererBaseProps {
|
||||||
@@ -116,7 +117,12 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
{{
|
{{
|
||||||
editor: {
|
editor: {
|
||||||
codemirror: (props: PassedEditorProps) => (
|
codemirror: (props: PassedEditorProps) => (
|
||||||
<CodeMirrorEditor {...props} lineNumbers={true} />
|
<MonacoEditor
|
||||||
|
{...props}
|
||||||
|
lineNumbers={true}
|
||||||
|
enableCompletion={true}
|
||||||
|
shouldRegisterDefaultCompletion={true}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import { List, Map } from "immutable";
|
|||||||
|
|
||||||
const fileName = "file";
|
const fileName = "file";
|
||||||
const notebookName = "file.ipynb";
|
const notebookName = "file.ipynb";
|
||||||
const filePath = `folder/${fileName}`;
|
const folderPath = "folder";
|
||||||
const notebookPath = `folder/${notebookName}`;
|
const filePath = `${folderPath}/${fileName}`;
|
||||||
|
const notebookPath = `${folderPath}/${notebookName}`;
|
||||||
|
const gitHubFolderUri = GitHubUtils.toContentUri("owner", "repo", "branch", folderPath);
|
||||||
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
||||||
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
||||||
const notebookRecord = makeNotebookRecord({
|
const notebookRecord = makeNotebookRecord({
|
||||||
@@ -43,10 +45,8 @@ const notebookRecord = makeNotebookRecord({
|
|||||||
source: 'display(HTML("<h1>Sample html</h1>"))',
|
source: 'display(HTML("<h1>Sample html</h1>"))',
|
||||||
outputs: List.of({
|
outputs: List.of({
|
||||||
data: Object.freeze({
|
data: Object.freeze({
|
||||||
data: {
|
"text/html": "<h1>Sample output</h1>",
|
||||||
"text/html": "<h1>Sample output</h1>",
|
"text/plain": "<IPython.core.display.HTML object>"
|
||||||
"text/plain": "<IPython.core.display.HTML object>"
|
|
||||||
}
|
|
||||||
} as MediaBundle),
|
} as MediaBundle),
|
||||||
output_type: "display_data",
|
output_type: "display_data",
|
||||||
metadata: undefined
|
metadata: undefined
|
||||||
@@ -82,6 +82,26 @@ describe("NotebookUtil", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getFilePath", () => {
|
||||||
|
it("works for jupyter file paths", () => {
|
||||||
|
expect(NotebookUtil.getFilePath(folderPath, fileName)).toEqual(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works for github file uris", () => {
|
||||||
|
expect(NotebookUtil.getFilePath(gitHubFolderUri, fileName)).toEqual(gitHubFileUri);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getParentPath", () => {
|
||||||
|
it("works for jupyter file paths", () => {
|
||||||
|
expect(NotebookUtil.getParentPath(filePath)).toEqual(folderPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works for github file uris", () => {
|
||||||
|
expect(NotebookUtil.getParentPath(gitHubFileUri)).toEqual(gitHubFolderUri);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getName", () => {
|
describe("getName", () => {
|
||||||
it("works for jupyter file paths", () => {
|
it("works for jupyter file paths", () => {
|
||||||
expect(NotebookUtil.getName(filePath)).toEqual(fileName);
|
expect(NotebookUtil.getName(filePath)).toEqual(fileName);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { ImmutableNotebook, ImmutableCodeCell, ImmutableOutput } from "@nteract/commutable";
|
import { ImmutableNotebook, ImmutableCodeCell } from "@nteract/commutable";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
import { StringUtils } from "../../Utils/StringUtils";
|
import { StringUtils } from "../../Utils/StringUtils";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
@@ -70,6 +70,46 @@ export class NotebookUtil {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getFilePath(path: string, fileName: string): string {
|
||||||
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
|
if (contentInfo) {
|
||||||
|
let path = fileName;
|
||||||
|
if (contentInfo.path) {
|
||||||
|
path = `${contentInfo.path}/${path}`;
|
||||||
|
}
|
||||||
|
return GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${path}/${fileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getParentPath(filepath: string): undefined | string {
|
||||||
|
const basename = NotebookUtil.getName(filepath);
|
||||||
|
if (basename) {
|
||||||
|
const contentInfo = GitHubUtils.fromContentUri(filepath);
|
||||||
|
if (contentInfo) {
|
||||||
|
const parentPath = contentInfo.path.split(basename).shift();
|
||||||
|
if (parentPath === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GitHubUtils.toContentUri(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
parentPath.replace(/\/$/, "") // no trailling slash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentPath = filepath.split(basename).shift();
|
||||||
|
if (parentPath) {
|
||||||
|
return parentPath.replace(/\/$/, ""); // no trailling slash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public static getName(path: string): undefined | string {
|
public static getName(path: string): undefined | string {
|
||||||
let relativePath: string = path;
|
let relativePath: string = path;
|
||||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
@@ -102,25 +142,19 @@ export class NotebookUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
|
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
|
||||||
let codeCellCount = -1;
|
let codeCellIndex = 0;
|
||||||
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
|
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
|
||||||
const cellId = notebookObject.cellOrder.get(i);
|
const cellId = notebookObject.cellOrder.get(i);
|
||||||
if (cellId) {
|
if (cellId) {
|
||||||
const cell = notebookObject.cellMap.get(cellId);
|
const cell = notebookObject.cellMap.get(cellId);
|
||||||
if (cell && cell.cell_type === "code") {
|
if (cell?.cell_type === "code") {
|
||||||
codeCellCount++;
|
const displayOutput = (cell as ImmutableCodeCell)?.outputs?.find(
|
||||||
const codeCell = cell as ImmutableCodeCell;
|
output => output.output_type === "display_data" || output.output_type === "execute_result"
|
||||||
if (codeCell.outputs) {
|
);
|
||||||
const displayOutput = codeCell.outputs.find((output: ImmutableOutput) => {
|
if (displayOutput) {
|
||||||
if (output.output_type === "display_data" || output.output_type === "execute_result") {
|
return codeCellIndex;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (displayOutput) {
|
|
||||||
return codeCellCount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
codeCellIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import EnvironmentUtility from "../../Common/EnvironmentUtility";
|
|||||||
import Q from "q";
|
import Q from "q";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { config, Platform } from "../../Config";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import { createMongoCollectionWithARM, createMongoCollectionWithProxy } from "../../Common/MongoProxyClient";
|
import { createMongoCollectionWithARM, createMongoCollectionWithProxy } from "../../Common/MongoProxyClient";
|
||||||
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
||||||
import { HashMap } from "../../Common/HashMap";
|
import { HashMap } from "../../Common/HashMap";
|
||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { refreshCachedResources, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
|
import { refreshCachedResources, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
||||||
isPreferredApiTable: ko.Computed<boolean>;
|
isPreferredApiTable: ko.Computed<boolean>;
|
||||||
@@ -599,7 +599,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.isSynapseLinkSupported = ko.computed(() => {
|
this.isSynapseLinkSupported = ko.computed(() => {
|
||||||
if (config.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,9 +920,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
partitionKey.version,
|
partitionKey.version,
|
||||||
databaseCreateNew,
|
databaseCreateNew,
|
||||||
useDatabaseSharedOffer,
|
useDatabaseSharedOffer,
|
||||||
CosmosClient.subscriptionId(),
|
userContext.subscriptionId,
|
||||||
CosmosClient.resourceGroup(),
|
userContext.resourceGroup,
|
||||||
CosmosClient.databaseAccount().name,
|
userContext.databaseAccount.name,
|
||||||
autopilotSettings
|
autopilotSettings
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -940,9 +940,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
partitionKey.version,
|
partitionKey.version,
|
||||||
databaseCreateNew,
|
databaseCreateNew,
|
||||||
useDatabaseSharedOffer,
|
useDatabaseSharedOffer,
|
||||||
CosmosClient.subscriptionId(),
|
userContext.subscriptionId,
|
||||||
CosmosClient.resourceGroup(),
|
userContext.resourceGroup,
|
||||||
CosmosClient.databaseAccount().name,
|
userContext.databaseAccount.name,
|
||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
autopilotSettings
|
autopilotSettings
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
|||||||
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
|
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
|
||||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class AddDatabasePane extends ContextualPaneBase {
|
export default class AddDatabasePane extends ContextualPaneBase {
|
||||||
public defaultExperience: ko.Computed<string>;
|
public defaultExperience: ko.Computed<string>;
|
||||||
@@ -304,76 +304,23 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
this.formErrors("");
|
this.formErrors("");
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
|
|
||||||
const createDatabaseParameters: DataModels.RpParameters = {
|
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||||
db: addDatabasePaneStartMessage.database.id,
|
autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(),
|
||||||
st: addDatabasePaneStartMessage.database.shared,
|
databaseId: addDatabasePaneStartMessage.database.id,
|
||||||
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
|
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
|
||||||
sid: CosmosClient.subscriptionId(),
|
offerThroughput: addDatabasePaneStartMessage.offerThroughput
|
||||||
rg: CosmosClient.resourceGroup(),
|
|
||||||
dba: addDatabasePaneStartMessage.databaseAccountName
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const autopilotSettings = this._getAutopilotSettings();
|
createDatabase(createDatabaseParams).then(
|
||||||
|
(database: DataModels.Database) => {
|
||||||
if (this.container.isPreferredApiCassandra()) {
|
this._onCreateDatabaseSuccess(offerThroughput, startKey);
|
||||||
this._createKeyspace(createDatabaseParameters, autopilotSettings, startKey);
|
},
|
||||||
} else if (this.container.isPreferredApiMongoDB() && EnvironmentUtility.isAadUser()) {
|
(reason: any) => {
|
||||||
this._createMongoDatabase(createDatabaseParameters, autopilotSettings, startKey);
|
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
|
||||||
} else if (this.container.isPreferredApiGraph() && EnvironmentUtility.isAadUser()) {
|
|
||||||
this._createGremlinDatabase(createDatabaseParameters, autopilotSettings, startKey);
|
|
||||||
} else if (this.container.isPreferredApiDocumentDB() && EnvironmentUtility.isAadUser()) {
|
|
||||||
this._createSqlDatabase(createDatabaseParameters, autopilotSettings, startKey);
|
|
||||||
} else {
|
|
||||||
this._createDatabase(offerThroughput, startKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createSqlDatabase(
|
|
||||||
createDatabaseParameters: DataModels.RpParameters,
|
|
||||||
autoPilotSettings: DataModels.RpOptions,
|
|
||||||
startKey: number
|
|
||||||
) {
|
|
||||||
AddDbUtilities.createSqlDatabase(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings).then(
|
|
||||||
() => {
|
|
||||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
|
||||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createMongoDatabase(
|
|
||||||
createDatabaseParameters: DataModels.RpParameters,
|
|
||||||
autoPilotSettings: DataModels.RpOptions,
|
|
||||||
startKey: number
|
|
||||||
) {
|
|
||||||
AddDbUtilities.createMongoDatabaseWithARM(
|
|
||||||
this.container.armEndpoint(),
|
|
||||||
createDatabaseParameters,
|
|
||||||
autoPilotSettings
|
|
||||||
).then(() => {
|
|
||||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
|
||||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createGremlinDatabase(
|
|
||||||
createDatabaseParameters: DataModels.RpParameters,
|
|
||||||
autoPilotSettings: DataModels.RpOptions,
|
|
||||||
startKey: number
|
|
||||||
) {
|
|
||||||
AddDbUtilities.createGremlinDatabase(
|
|
||||||
this.container.armEndpoint(),
|
|
||||||
createDatabaseParameters,
|
|
||||||
autoPilotSettings
|
|
||||||
).then(() => {
|
|
||||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
|
||||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetData() {
|
public resetData() {
|
||||||
this.databaseId("");
|
this.databaseId("");
|
||||||
this.databaseCreateNewShared(this.getSharedThroughputDefault());
|
this.databaseCreateNewShared(this.getSharedThroughputDefault());
|
||||||
@@ -396,72 +343,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createDatabase(offerThroughput: number, telemetryStartKey: number): void {
|
|
||||||
const autoPilot: DataModels.AutoPilotCreationSettings = this._isAutoPilotSelectedAndWhatTier();
|
|
||||||
const createRequest: DataModels.CreateDatabaseRequest = {
|
|
||||||
databaseId: this.databaseId().trim(),
|
|
||||||
offerThroughput,
|
|
||||||
databaseLevelThroughput: this.databaseCreateNewShared(),
|
|
||||||
autoPilot,
|
|
||||||
hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag()
|
|
||||||
};
|
|
||||||
createDatabase(createRequest).then(
|
|
||||||
(database: DataModels.Database) => {
|
|
||||||
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
|
|
||||||
},
|
|
||||||
(reason: any) => {
|
|
||||||
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createKeyspace(
|
|
||||||
createDatabaseParameters: DataModels.RpParameters,
|
|
||||||
autoPilotSettings: DataModels.RpOptions,
|
|
||||||
startKey: number
|
|
||||||
): void {
|
|
||||||
if (EnvironmentUtility.isAadUser()) {
|
|
||||||
this._createKeyspaceUsingRP(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings, startKey);
|
|
||||||
} else {
|
|
||||||
this._createKeyspaceUsingProxy(createDatabaseParameters.offerThroughput, startKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createKeyspaceUsingProxy(offerThroughput: number, telemetryStartKey: number): void {
|
|
||||||
const provisionThroughputQueryPart: string = this.databaseCreateNewShared()
|
|
||||||
? `AND cosmosdb_provisioned_throughput=${offerThroughput}`
|
|
||||||
: "";
|
|
||||||
const createKeyspaceQuery: string = `CREATE KEYSPACE ${this.databaseId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 } ${provisionThroughputQueryPart};`;
|
|
||||||
(this.container.tableDataClient as CassandraAPIDataClient)
|
|
||||||
.createKeyspace(
|
|
||||||
this.container.databaseAccount().properties.cassandraEndpoint,
|
|
||||||
this.container.databaseAccount().id,
|
|
||||||
this.container,
|
|
||||||
createKeyspaceQuery
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
|
|
||||||
},
|
|
||||||
(reason: any) => {
|
|
||||||
this._onCreateDatabaseFailure(reason, offerThroughput, telemetryStartKey);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createKeyspaceUsingRP(
|
|
||||||
armEndpoint: string,
|
|
||||||
createKeyspaceParameters: DataModels.RpParameters,
|
|
||||||
autoPilotSettings: DataModels.RpOptions,
|
|
||||||
startKey: number
|
|
||||||
): void {
|
|
||||||
AddDbUtilities.createCassandraKeyspace(armEndpoint, createKeyspaceParameters, autoPilotSettings).then(() => {
|
|
||||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
|
||||||
this._onCreateDatabaseSuccess(createKeyspaceParameters.offerThroughput, startKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
|
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
|
||||||
this.isExecuting(false);
|
this.isExecuting(false);
|
||||||
this.close();
|
this.close();
|
||||||
@@ -582,20 +463,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getAutopilotSettings(): DataModels.RpOptions {
|
|
||||||
if (
|
|
||||||
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) ||
|
|
||||||
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier())
|
|
||||||
) {
|
|
||||||
return !this.hasAutoPilotV2FeatureFlag()
|
|
||||||
? {
|
|
||||||
[Constants.HttpHeaders.autoPilotThroughput]: { maxThroughput: this.maxAutoPilotThroughputSet() * 1 }
|
|
||||||
}
|
|
||||||
: { [Constants.HttpHeaders.autoPilotTier]: this.selectedAutoPilotTier().toString() };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateThroughputLimitByDatabase() {
|
private _updateThroughputLimitByDatabase() {
|
||||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||||
this.throughput(throughputDefaults.shared);
|
this.throughput(throughputDefaults.shared);
|
||||||
|
|||||||
198
src/Explorer/Panes/CopyNotebookPane.tsx
Normal file
198
src/Explorer/Panes/CopyNotebookPane.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import ko from "knockout";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
|
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
||||||
|
import { IDropdownOption } from "office-ui-fabric-react";
|
||||||
|
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||||
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
|
|
||||||
|
interface Location {
|
||||||
|
type: "MyNotebooks" | "GitHub";
|
||||||
|
|
||||||
|
// GitHub
|
||||||
|
owner?: string;
|
||||||
|
repo?: string;
|
||||||
|
branch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyNotebookPaneAdapter implements ReactAdapter {
|
||||||
|
private static readonly BranchNameWhiteSpace = " ";
|
||||||
|
|
||||||
|
parameters: ko.Observable<number>;
|
||||||
|
private isOpened: boolean;
|
||||||
|
private isExecuting: boolean;
|
||||||
|
private formError: string;
|
||||||
|
private formErrorDetail: string;
|
||||||
|
private name: string;
|
||||||
|
private content: string;
|
||||||
|
private pinnedRepos: IPinnedRepo[];
|
||||||
|
private selectedLocation: Location;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private container: Explorer,
|
||||||
|
private junoClient: JunoClient,
|
||||||
|
private gitHubOAuthService: GitHubOAuthService
|
||||||
|
) {
|
||||||
|
this.parameters = ko.observable(Date.now());
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
if (!this.isOpened) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const genericPaneProps: GenericRightPaneProps = {
|
||||||
|
container: this.container,
|
||||||
|
formError: this.formError,
|
||||||
|
formErrorDetail: this.formErrorDetail,
|
||||||
|
id: "copynotebookpane",
|
||||||
|
isExecuting: this.isExecuting,
|
||||||
|
title: "Copy notebook",
|
||||||
|
submitButtonText: "OK",
|
||||||
|
onClose: () => this.close(),
|
||||||
|
onSubmit: () => this.submit()
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyNotebookPaneProps: CopyNotebookPaneProps = {
|
||||||
|
name: this.name,
|
||||||
|
pinnedRepos: this.pinnedRepos,
|
||||||
|
onDropDownChange: this.onDropDownChange
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericRightPaneComponent {...genericPaneProps}>
|
||||||
|
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
|
||||||
|
</GenericRightPaneComponent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerRender(): void {
|
||||||
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async open(name: string, content: string): Promise<void> {
|
||||||
|
this.name = name;
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
this.isOpened = true;
|
||||||
|
this.triggerRender();
|
||||||
|
|
||||||
|
if (this.gitHubOAuthService.isLoggedIn()) {
|
||||||
|
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
|
const message = `Received HTTP ${response.status} when fetching pinned repos`;
|
||||||
|
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
|
||||||
|
NotificationConsoleUtils.logConsoleError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.length > 0) {
|
||||||
|
this.pinnedRepos = response.data;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async submit(): Promise<void> {
|
||||||
|
let destination: string = this.selectedLocation?.type;
|
||||||
|
let clearMessage: () => void;
|
||||||
|
this.isExecuting = true;
|
||||||
|
this.triggerRender();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.selectedLocation) {
|
||||||
|
throw new Error(`No location selected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedLocation.type === "GitHub") {
|
||||||
|
destination = `${destination} - ${GitHubUtils.toRepoFullName(
|
||||||
|
this.selectedLocation.owner,
|
||||||
|
this.selectedLocation.repo
|
||||||
|
)} - ${this.selectedLocation.branch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${this.name} to ${destination}`);
|
||||||
|
|
||||||
|
const notebookContentItem = await this.copyNotebook(this.selectedLocation);
|
||||||
|
if (!notebookContentItem) {
|
||||||
|
throw new Error(`Failed to upload ${this.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.formError = `Failed to copy ${this.name} to ${destination}`;
|
||||||
|
this.formErrorDetail = `${error}`;
|
||||||
|
|
||||||
|
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||||
|
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
|
||||||
|
NotificationConsoleUtils.logConsoleError(message);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
clearMessage && clearMessage();
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
|
||||||
|
let parent: NotebookContentItem;
|
||||||
|
switch (location.type) {
|
||||||
|
case "MyNotebooks":
|
||||||
|
parent = {
|
||||||
|
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
path: this.container.getNotebookBasePath(),
|
||||||
|
type: NotebookContentItemType.Directory
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GitHub":
|
||||||
|
parent = {
|
||||||
|
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
|
path: GitHubUtils.toContentUri(
|
||||||
|
this.selectedLocation.owner,
|
||||||
|
this.selectedLocation.repo,
|
||||||
|
this.selectedLocation.branch,
|
||||||
|
""
|
||||||
|
),
|
||||||
|
type: NotebookContentItemType.Directory
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported location type ${location.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.container.uploadFile(this.name, this.content, parent);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||||
|
this.selectedLocation = option?.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
private reset = (): void => {
|
||||||
|
this.isOpened = false;
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.formError = undefined;
|
||||||
|
this.formErrorDetail = undefined;
|
||||||
|
this.name = undefined;
|
||||||
|
this.content = undefined;
|
||||||
|
this.pinnedRepos = undefined;
|
||||||
|
this.selectedLocation = undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
119
src/Explorer/Panes/CopyNotebookPaneComponent.tsx
Normal file
119
src/Explorer/Panes/CopyNotebookPaneComponent.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import * as React from "react";
|
||||||
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Label,
|
||||||
|
Text,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownProps,
|
||||||
|
IDropdownOption,
|
||||||
|
SelectableOptionMenuItemType,
|
||||||
|
IRenderFunction,
|
||||||
|
ISelectableOption
|
||||||
|
} from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
interface Location {
|
||||||
|
type: "MyNotebooks" | "GitHub";
|
||||||
|
|
||||||
|
// GitHub
|
||||||
|
owner?: string;
|
||||||
|
repo?: string;
|
||||||
|
branch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopyNotebookPaneProps {
|
||||||
|
name: string;
|
||||||
|
pinnedRepos: IPinnedRepo[];
|
||||||
|
onDropDownChange: (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneProps> {
|
||||||
|
private static readonly BranchNameWhiteSpace = " ";
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const dropDownProps: IDropdownProps = {
|
||||||
|
label: "Location",
|
||||||
|
ariaLabel: "Location",
|
||||||
|
placeholder: "Select an option",
|
||||||
|
onRenderTitle: this.onRenderDropDownTitle,
|
||||||
|
onRenderOption: this.onRenderDropDownOption,
|
||||||
|
options: this.getDropDownOptions(),
|
||||||
|
onChange: this.props.onDropDownChange
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="paneMainContent">
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label htmlFor="notebookName">Name</Label>
|
||||||
|
<Text id="notebookName">{this.props.name}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Dropdown {...dropDownProps} />
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
||||||
|
return <span>{options.length && options[0].title}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
|
||||||
|
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getDropDownOptions = (): IDropdownOption[] => {
|
||||||
|
const options: IDropdownOption[] = [];
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key: "MyNotebooks-Item",
|
||||||
|
text: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
title: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
data: {
|
||||||
|
type: "MyNotebooks"
|
||||||
|
} as Location
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.props.pinnedRepos && this.props.pinnedRepos.length > 0) {
|
||||||
|
options.push({
|
||||||
|
key: "GitHub-Header-Divider",
|
||||||
|
text: undefined,
|
||||||
|
itemType: SelectableOptionMenuItemType.Divider
|
||||||
|
});
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key: "GitHub-Header",
|
||||||
|
text: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
|
itemType: SelectableOptionMenuItemType.Header
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.pinnedRepos.forEach(pinnedRepo => {
|
||||||
|
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||||
|
options.push({
|
||||||
|
key: `GitHub-Repo-${repoFullName}`,
|
||||||
|
text: repoFullName,
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
pinnedRepo.branches.forEach(branch =>
|
||||||
|
options.push({
|
||||||
|
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
|
||||||
|
text: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`,
|
||||||
|
title: `${repoFullName} - ${branch.name}`,
|
||||||
|
data: {
|
||||||
|
type: "GitHub",
|
||||||
|
owner: pinnedRepo.owner,
|
||||||
|
repo: pinnedRepo.name,
|
||||||
|
branch: branch.name
|
||||||
|
} as Location
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import Explorer from "../Explorer";
|
|||||||
|
|
||||||
export interface GenericRightPaneProps {
|
export interface GenericRightPaneProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
content: JSX.Element;
|
|
||||||
formError: string;
|
formError: string;
|
||||||
formErrorDetail: string;
|
formErrorDetail: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,6 +16,7 @@ export interface GenericRightPaneProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
submitButtonText: string;
|
submitButtonText: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
isSubmitButtonHidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericRightPaneState {
|
export interface GenericRightPaneState {
|
||||||
@@ -56,18 +56,18 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
<div className="panelContentWrapper">
|
<div className="panelContentWrapper">
|
||||||
{this.createPanelHeader()}
|
{this.renderPanelHeader()}
|
||||||
{this.createErrorSection()}
|
{this.renderErrorSection()}
|
||||||
{this.props.content}
|
{this.props.children}
|
||||||
{this.createPanelFooter()}
|
{this.renderPanelFooter()}
|
||||||
</div>
|
</div>
|
||||||
{this.createLoadingScreen()}
|
{this.renderLoadingScreen()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPanelHeader = (): JSX.Element => {
|
private renderPanelHeader = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="firstdivbg headerline">
|
<div className="firstdivbg headerline">
|
||||||
<span id="databaseTitle">{this.props.title}</span>
|
<span id="databaseTitle">{this.props.title}</span>
|
||||||
@@ -83,7 +83,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private createErrorSection = (): JSX.Element => {
|
private renderErrorSection = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
|
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
|
||||||
<div className="warningErrorContent">
|
<div className="warningErrorContent">
|
||||||
@@ -103,11 +103,12 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private createPanelFooter = (): JSX.Element => {
|
private renderPanelFooter = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="paneFooter">
|
<div className="paneFooter">
|
||||||
<div className="leftpanel-okbut">
|
<div className="leftpanel-okbut">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
style={{ visibility: this.props.isSubmitButtonHidden ? "hidden" : "visible" }}
|
||||||
ariaLabel="Submit"
|
ariaLabel="Submit"
|
||||||
title="Submit"
|
title="Submit"
|
||||||
onClick={this.props.onSubmit}
|
onClick={this.props.onSubmit}
|
||||||
@@ -120,7 +121,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private createLoadingScreen = (): JSX.Element => {
|
private renderLoadingScreen = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
|
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
|
||||||
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
|
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRight
|
|||||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||||
import { ImmutableNotebook } from "@nteract/commutable/src";
|
import { ImmutableNotebook } from "@nteract/commutable/src";
|
||||||
import { toJS } from "@nteract/commutable";
|
import { toJS } from "@nteract/commutable";
|
||||||
|
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
|
||||||
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
|
|
||||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
parameters: ko.Observable<number>;
|
parameters: ko.Observable<number>;
|
||||||
@@ -26,6 +28,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
private imageSrc: string;
|
private imageSrc: string;
|
||||||
private notebookObject: ImmutableNotebook;
|
private notebookObject: ImmutableNotebook;
|
||||||
private parentDomElement: HTMLElement;
|
private parentDomElement: HTMLElement;
|
||||||
|
private isCodeOfConductAccepted: boolean;
|
||||||
|
private isLinkInjectionEnabled: boolean;
|
||||||
|
|
||||||
constructor(private container: Explorer, private junoClient: JunoClient) {
|
constructor(private container: Explorer, private junoClient: JunoClient) {
|
||||||
this.parameters = ko.observable(Date.now());
|
this.parameters = ko.observable(Date.now());
|
||||||
@@ -40,7 +44,6 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
const props: GenericRightPaneProps = {
|
const props: GenericRightPaneProps = {
|
||||||
container: this.container,
|
container: this.container,
|
||||||
content: this.createContent(),
|
|
||||||
formError: this.formError,
|
formError: this.formError,
|
||||||
formErrorDetail: this.formErrorDetail,
|
formErrorDetail: this.formErrorDetail,
|
||||||
id: "publishnotebookpane",
|
id: "publishnotebookpane",
|
||||||
@@ -48,33 +51,86 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
title: "Publish to gallery",
|
title: "Publish to gallery",
|
||||||
submitButtonText: "Publish",
|
submitButtonText: "Publish",
|
||||||
onClose: () => this.close(),
|
onClose: () => this.close(),
|
||||||
onSubmit: () => this.submit()
|
onSubmit: () => this.submit(),
|
||||||
|
isSubmitButtonHidden: !this.isCodeOfConductAccepted
|
||||||
};
|
};
|
||||||
|
|
||||||
return <GenericRightPaneComponent {...props} />;
|
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||||
|
notebookName: this.name,
|
||||||
|
notebookDescription: "",
|
||||||
|
notebookTags: "",
|
||||||
|
notebookAuthor: this.author,
|
||||||
|
notebookCreatedDate: new Date().toISOString(),
|
||||||
|
notebookObject: this.notebookObject,
|
||||||
|
notebookParentDomElement: this.parentDomElement,
|
||||||
|
onChangeName: (newValue: string) => (this.name = newValue),
|
||||||
|
onChangeDescription: (newValue: string) => (this.description = newValue),
|
||||||
|
onChangeTags: (newValue: string) => (this.tags = newValue),
|
||||||
|
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
||||||
|
onError: this.createFormErrorForLargeImageSelection,
|
||||||
|
clearFormError: this.clearFormError
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericRightPaneComponent {...props}>
|
||||||
|
{!this.isCodeOfConductAccepted ? (
|
||||||
|
<div style={{ padding: "15px", marginTop: "10px" }}>
|
||||||
|
<CodeOfConductComponent
|
||||||
|
junoClient={this.junoClient}
|
||||||
|
onAcceptCodeOfConduct={() => {
|
||||||
|
this.isCodeOfConductAccepted = true;
|
||||||
|
this.triggerRender();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
|
||||||
|
)}
|
||||||
|
</GenericRightPaneComponent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public triggerRender(): void {
|
public triggerRender(): void {
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public open(
|
public async open(
|
||||||
name: string,
|
name: string,
|
||||||
author: string,
|
author: string,
|
||||||
notebookContent: string | ImmutableNotebook,
|
notebookContent: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement
|
parentDomElement: HTMLElement,
|
||||||
): void {
|
isCodeOfConductEnabled: boolean,
|
||||||
|
isLinkInjectionEnabled: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
if (isCodeOfConductEnabled) {
|
||||||
|
try {
|
||||||
|
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
|
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isCodeOfConductAccepted = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const message = `Failed to check if code of conduct was accepted: ${error}`;
|
||||||
|
Logger.logError(message, "PublishNotebookPaneAdapter/isCodeOfConductAccepted");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isCodeOfConductAccepted = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
if (typeof notebookContent === "string") {
|
if (typeof notebookContent === "string") {
|
||||||
this.content = notebookContent as string;
|
this.content = notebookContent as string;
|
||||||
} else {
|
} else {
|
||||||
this.content = JSON.stringify(toJS(notebookContent as ImmutableNotebook));
|
this.content = JSON.stringify(toJS(notebookContent));
|
||||||
this.notebookObject = notebookContent;
|
this.notebookObject = notebookContent;
|
||||||
}
|
}
|
||||||
this.parentDomElement = parentDomElement;
|
this.parentDomElement = parentDomElement;
|
||||||
|
|
||||||
this.isOpened = true;
|
this.isOpened = true;
|
||||||
|
this.isLinkInjectionEnabled = isLinkInjectionEnabled;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +158,12 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.tags?.split(","),
|
this.tags?.split(","),
|
||||||
this.author,
|
this.author,
|
||||||
this.imageSrc,
|
this.imageSrc,
|
||||||
this.content
|
this.content,
|
||||||
|
this.isLinkInjectionEnabled
|
||||||
);
|
);
|
||||||
if (!response.data) {
|
if (response.data) {
|
||||||
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||||
this.formErrorDetail = `${error}`;
|
this.formErrorDetail = `${error}`;
|
||||||
@@ -142,25 +197,6 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
private createContent = (): JSX.Element => {
|
|
||||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
|
||||||
notebookName: this.name,
|
|
||||||
notebookDescription: "",
|
|
||||||
notebookTags: "",
|
|
||||||
notebookAuthor: this.author,
|
|
||||||
notebookCreatedDate: new Date().toISOString(),
|
|
||||||
notebookObject: this.notebookObject,
|
|
||||||
notebookParentDomElement: this.parentDomElement,
|
|
||||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
|
||||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
|
||||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
|
||||||
onError: this.createFormErrorForLargeImageSelection,
|
|
||||||
clearFormError: this.clearFormError
|
|
||||||
};
|
|
||||||
|
|
||||||
return <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
private reset = (): void => {
|
private reset = (): void => {
|
||||||
this.isOpened = false;
|
this.isOpened = false;
|
||||||
this.isExecuting = false;
|
this.isExecuting = false;
|
||||||
@@ -174,5 +210,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.imageSrc = undefined;
|
this.imageSrc = undefined;
|
||||||
this.notebookObject = undefined;
|
this.notebookObject = undefined;
|
||||||
this.parentDomElement = undefined;
|
this.parentDomElement = undefined;
|
||||||
|
this.isCodeOfConductAccepted = undefined;
|
||||||
|
this.isLinkInjectionEnabled = undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe("PublishNotebookPaneComponent", () => {
|
|||||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||||
notebookObject: undefined,
|
notebookObject: undefined,
|
||||||
notebookParentDomElement: undefined,
|
notebookParentDomElement: undefined,
|
||||||
|
onChangeName: undefined,
|
||||||
onChangeDescription: undefined,
|
onChangeDescription: undefined,
|
||||||
onChangeTags: undefined,
|
onChangeTags: undefined,
|
||||||
onChangeImageSrc: undefined,
|
onChangeImageSrc: undefined,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface PublishNotebookPaneProps {
|
|||||||
notebookCreatedDate: string;
|
notebookCreatedDate: string;
|
||||||
notebookObject: ImmutableNotebook;
|
notebookObject: ImmutableNotebook;
|
||||||
notebookParentDomElement: HTMLElement;
|
notebookParentDomElement: HTMLElement;
|
||||||
|
onChangeName: (newValue: string) => void;
|
||||||
onChangeDescription: (newValue: string) => void;
|
onChangeDescription: (newValue: string) => void;
|
||||||
onChangeTags: (newValue: string) => void;
|
onChangeTags: (newValue: string) => void;
|
||||||
onChangeImageSrc: (newValue: string) => void;
|
onChangeImageSrc: (newValue: string) => void;
|
||||||
@@ -24,6 +25,7 @@ export interface PublishNotebookPaneProps {
|
|||||||
|
|
||||||
interface PublishNotebookPaneState {
|
interface PublishNotebookPaneState {
|
||||||
type: string;
|
type: string;
|
||||||
|
notebookName: string;
|
||||||
notebookDescription: string;
|
notebookDescription: string;
|
||||||
notebookTags: string;
|
notebookTags: string;
|
||||||
imageSrc: string;
|
imageSrc: string;
|
||||||
@@ -40,6 +42,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
private static readonly maxImageSizeInMib = 1.5;
|
private static readonly maxImageSizeInMib = 1.5;
|
||||||
private descriptionPara1: string;
|
private descriptionPara1: string;
|
||||||
private descriptionPara2: string;
|
private descriptionPara2: string;
|
||||||
|
private nameProps: ITextFieldProps;
|
||||||
private descriptionProps: ITextFieldProps;
|
private descriptionProps: ITextFieldProps;
|
||||||
private tagsProps: ITextFieldProps;
|
private tagsProps: ITextFieldProps;
|
||||||
private thumbnailUrlProps: ITextFieldProps;
|
private thumbnailUrlProps: ITextFieldProps;
|
||||||
@@ -52,6 +55,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
type: ImageTypes.Url,
|
type: ImageTypes.Url,
|
||||||
|
notebookName: props.notebookName,
|
||||||
notebookDescription: "",
|
notebookDescription: "",
|
||||||
notebookTags: "",
|
notebookTags: "",
|
||||||
imageSrc: undefined
|
imageSrc: undefined
|
||||||
@@ -165,6 +169,17 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.nameProps = {
|
||||||
|
label: "Name",
|
||||||
|
ariaLabel: "Name",
|
||||||
|
defaultValue: this.props.notebookName,
|
||||||
|
required: true,
|
||||||
|
onChange: (event, newValue) => {
|
||||||
|
this.props.onChangeName(newValue);
|
||||||
|
this.setState({ notebookName: newValue });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.descriptionProps = {
|
this.descriptionProps = {
|
||||||
label: "Description",
|
label: "Description",
|
||||||
ariaLabel: "Description",
|
ariaLabel: "Description",
|
||||||
@@ -245,6 +260,10 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
<Text>{this.descriptionPara2}</Text>
|
<Text>{this.descriptionPara2}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<TextField {...this.nameProps} />
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<TextField {...this.descriptionProps} />
|
<TextField {...this.descriptionProps} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@@ -266,7 +285,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
<GalleryCardComponent
|
<GalleryCardComponent
|
||||||
data={{
|
data={{
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: this.props.notebookName,
|
name: this.state.notebookName,
|
||||||
description: this.state.notebookDescription,
|
description: this.state.notebookDescription,
|
||||||
gitSha: undefined,
|
gitSha: undefined,
|
||||||
tags: this.state.notebookTags.split(","),
|
tags: this.state.notebookTags.split(","),
|
||||||
@@ -276,7 +295,8 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
}}
|
}}
|
||||||
isFavorite={false}
|
isFavorite={false}
|
||||||
showDownload={true}
|
showDownload={true}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
|||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { StringUtility } from "../../Shared/StringUtility";
|
import { StringUtility } from "../../Shared/StringUtility";
|
||||||
import { config } from "../../Config";
|
import { configContext } from "../../ConfigContext";
|
||||||
|
|
||||||
export class SettingsPane extends ContextualPaneBase {
|
export class SettingsPane extends ContextualPaneBase {
|
||||||
public pageOption: ko.Observable<string>;
|
public pageOption: ko.Observable<string>;
|
||||||
@@ -46,7 +46,7 @@ export class SettingsPane extends ContextualPaneBase {
|
|||||||
: false;
|
: false;
|
||||||
this.graphAutoVizDisabled = ko.observable<string>(`${isGraphAutoVizDisabled}`);
|
this.graphAutoVizDisabled = ko.observable<string>(`${isGraphAutoVizDisabled}`);
|
||||||
|
|
||||||
this.explorerVersion = config.gitSha;
|
this.explorerVersion = configContext.gitSha;
|
||||||
this.shouldShowQueryPageOptions = ko.computed<boolean>(() => this.container.isPreferredApiDocumentDB());
|
this.shouldShowQueryPageOptions = ko.computed<boolean>(() => this.container.isPreferredApiDocumentDB());
|
||||||
this.shouldShowCrossPartitionOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
|
this.shouldShowCrossPartitionOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
|
||||||
this.shouldShowParallelismOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
|
this.shouldShowParallelismOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import * as Constants from "../../Common/Constants";
|
|
||||||
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
|
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { IconButton } from "office-ui-fabric-react/lib/Button";
|
|
||||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
|
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
|
||||||
import InfoBubbleIcon from "../../../images/info-bubble.svg";
|
import { UploadItemsPaneComponent, UploadItemsPaneProps } from "./UploadItemsPaneComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
|
||||||
const UPLOAD_FILE_SIZE_LIMIT = 2097152;
|
const UPLOAD_FILE_SIZE_LIMIT = 2097152;
|
||||||
@@ -35,9 +33,8 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props: GenericRightPaneProps = {
|
const genericPaneProps: GenericRightPaneProps = {
|
||||||
container: this.container,
|
container: this.container,
|
||||||
content: this.createContent(),
|
|
||||||
formError: this.formError,
|
formError: this.formError,
|
||||||
formErrorDetail: this.formErrorDetail,
|
formErrorDetail: this.formErrorDetail,
|
||||||
id: "uploaditemspane",
|
id: "uploaditemspane",
|
||||||
@@ -47,7 +44,18 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
|||||||
onClose: () => this.close(),
|
onClose: () => this.close(),
|
||||||
onSubmit: () => this.submit()
|
onSubmit: () => this.submit()
|
||||||
};
|
};
|
||||||
return <GenericRightPaneComponent {...props} />;
|
|
||||||
|
const uploadItemsPaneProps: UploadItemsPaneProps = {
|
||||||
|
selectedFilesTitle: this.selectedFilesTitle,
|
||||||
|
updateSelectedFiles: this.updateSelectedFiles,
|
||||||
|
uploadFileData: this.uploadFileData
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericRightPaneComponent {...genericPaneProps}>
|
||||||
|
<UploadItemsPaneComponent {...uploadItemsPaneProps} />
|
||||||
|
</GenericRightPaneComponent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public triggerRender(): void {
|
public triggerRender(): void {
|
||||||
@@ -110,77 +118,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private createContent = (): JSX.Element => {
|
|
||||||
return <div className="panelContent">{this.createMainContentSection()}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
private createMainContentSection = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<div className="paneMainContent">
|
|
||||||
<div className="renewUploadItemsHeader">
|
|
||||||
<span> Select JSON Files </span>
|
|
||||||
<span className="infoTooltip" role="tooltip" tabIndex={0}>
|
|
||||||
<img className="infoImg" src={InfoBubbleIcon} alt="More information" />
|
|
||||||
<span className="tooltiptext infoTooltipWidth">
|
|
||||||
Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON
|
|
||||||
documents. The combined size of all files in an individual upload operation must be less than 2 MB. You
|
|
||||||
can perform multiple upload operations for larger data sets.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="importFilesTitle"
|
|
||||||
type="text"
|
|
||||||
disabled
|
|
||||||
value={this.selectedFilesTitle}
|
|
||||||
aria-label="Select JSON Files"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="importDocsInput"
|
|
||||||
title="Upload Icon"
|
|
||||||
multiple
|
|
||||||
accept="application/json"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={this.updateSelectedFiles}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "FolderHorizontal" }}
|
|
||||||
className="fileImportButton"
|
|
||||||
alt="Select JSON files to upload"
|
|
||||||
title="Select JSON files to upload"
|
|
||||||
onClick={this.onImportButtonClick}
|
|
||||||
onKeyPress={this.onImportButtonKeyPress}
|
|
||||||
/>
|
|
||||||
<div className="fileUploadSummaryContainer" hidden={this.uploadFileData.length === 0}>
|
|
||||||
<b>File upload status</b>
|
|
||||||
<table className="fileUploadSummary">
|
|
||||||
<thead>
|
|
||||||
<tr className="fileUploadSummaryHeader fileUploadSummaryTuple">
|
|
||||||
<th>FILE NAME</th>
|
|
||||||
<th>STATUS</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{this.uploadFileData.map(
|
|
||||||
(data: UploadDetailsRecord): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<tr className="fileUploadSummaryTuple" key={data.fileName}>
|
|
||||||
<td>{data.fileName}</td>
|
|
||||||
<td>{this.fileUploadSummaryText(data.numSucceeded, data.numFailed)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateSelectedFiles = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
private updateSelectedFiles = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.selectedFiles = event.target.files;
|
this.selectedFiles = event.target.files;
|
||||||
this._updateSelectedFilesTitle();
|
this._updateSelectedFilesTitle();
|
||||||
@@ -212,21 +149,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
|||||||
return totalFileSize;
|
return totalFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
|
|
||||||
return `${numSucceeded} items created, ${numFailed} errors`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onImportButtonClick = (): void => {
|
|
||||||
document.getElementById("importDocsInput").click();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onImportButtonKeyPress = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
|
|
||||||
if (event.charCode === Constants.KeyCodes.Enter || event.charCode === Constants.KeyCodes.Space) {
|
|
||||||
this.onImportButtonClick();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private reset = (): void => {
|
private reset = (): void => {
|
||||||
this.isOpened = false;
|
this.isOpened = false;
|
||||||
this.isExecuting = false;
|
this.isExecuting = false;
|
||||||
|
|||||||
97
src/Explorer/Panes/UploadItemsPaneComponent.tsx
Normal file
97
src/Explorer/Panes/UploadItemsPaneComponent.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import * as React from "react";
|
||||||
|
import { IconButton } from "office-ui-fabric-react/lib/Button";
|
||||||
|
import { UploadDetailsRecord } from "../../workers/upload/definitions";
|
||||||
|
import InfoBubbleIcon from "../../../images/info-bubble.svg";
|
||||||
|
|
||||||
|
export interface UploadItemsPaneProps {
|
||||||
|
selectedFilesTitle: string;
|
||||||
|
updateSelectedFiles: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
uploadFileData: UploadDetailsRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UploadItemsPaneComponent extends React.Component<UploadItemsPaneProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="panelContent">
|
||||||
|
<div className="paneMainContent">
|
||||||
|
<div className="renewUploadItemsHeader">
|
||||||
|
<span> Select JSON Files </span>
|
||||||
|
<span className="infoTooltip" role="tooltip" tabIndex={0}>
|
||||||
|
<img className="infoImg" src={InfoBubbleIcon} alt="More information" />
|
||||||
|
<span className="tooltiptext infoTooltipWidth">
|
||||||
|
Select one or more JSON files to upload. Each file can contain a single JSON document or an array of
|
||||||
|
JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB.
|
||||||
|
You can perform multiple upload operations for larger data sets.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="importFilesTitle"
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
value={this.props.selectedFilesTitle}
|
||||||
|
aria-label="Select JSON Files"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="importDocsInput"
|
||||||
|
title="Upload Icon"
|
||||||
|
multiple
|
||||||
|
accept="application/json"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={this.props.updateSelectedFiles}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "FolderHorizontal" }}
|
||||||
|
className="fileImportButton"
|
||||||
|
alt="Select JSON files to upload"
|
||||||
|
title="Select JSON files to upload"
|
||||||
|
onClick={this.onImportButtonClick}
|
||||||
|
onKeyPress={this.onImportButtonKeyPress}
|
||||||
|
/>
|
||||||
|
<div className="fileUploadSummaryContainer" hidden={this.props.uploadFileData.length === 0}>
|
||||||
|
<b>File upload status</b>
|
||||||
|
<table className="fileUploadSummary">
|
||||||
|
<thead>
|
||||||
|
<tr className="fileUploadSummaryHeader fileUploadSummaryTuple">
|
||||||
|
<th>FILE NAME</th>
|
||||||
|
<th>STATUS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.props.uploadFileData.map(
|
||||||
|
(data: UploadDetailsRecord): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<tr className="fileUploadSummaryTuple" key={data.fileName}>
|
||||||
|
<td>{data.fileName}</td>
|
||||||
|
<td>{this.fileUploadSummaryText(data.numSucceeded, data.numFailed)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
|
||||||
|
return `${numSucceeded} items created, ${numFailed} errors`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onImportButtonClick = (): void => {
|
||||||
|
document.getElementById("importDocsInput").click();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onImportButtonKeyPress = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
|
||||||
|
if (event.charCode === Constants.KeyCodes.Enter || event.charCode === Constants.KeyCodes.Space) {
|
||||||
|
this.onImportButtonClick();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,6 +22,15 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
Would you like to publish and share "SampleNotebook" to the gallery?
|
Would you like to publish and share "SampleNotebook" to the gallery?
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
ariaLabel="Name"
|
||||||
|
defaultValue="SampleNotebook.ipynb"
|
||||||
|
label="Name"
|
||||||
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Description"
|
ariaLabel="Description"
|
||||||
@@ -93,6 +102,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"id": undefined,
|
"id": undefined,
|
||||||
"isSample": false,
|
"isSample": false,
|
||||||
"name": "SampleNotebook.ipynb",
|
"name": "SampleNotebook.ipynb",
|
||||||
|
"newCellId": undefined,
|
||||||
"tags": Array [
|
"tags": Array [
|
||||||
"",
|
"",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import * as ko from "knockout";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
|
|
||||||
import NewContainerIcon from "../../../images/Hero-new-container.svg";
|
import NewContainerIcon from "../../../images/Hero-new-container.svg";
|
||||||
import NewNotebookIcon from "../../../images/Hero-new-notebook.svg";
|
import NewNotebookIcon from "../../../images/Hero-new-notebook.svg";
|
||||||
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
|
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
|
||||||
@@ -19,6 +17,7 @@ import AddDatabaseIcon from "../../../images/AddDatabase.svg";
|
|||||||
import SampleIcon from "../../../images/Hero-sample.svg";
|
import SampleIcon from "../../../images/Hero-sample.svg";
|
||||||
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO Remove this when fully ported to ReactJS
|
* TODO Remove this when fully ported to ReactJS
|
||||||
@@ -46,7 +45,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private clearMostRecent = (): void => {
|
private clearMostRecent = (): void => {
|
||||||
this.container.mostRecentActivity.clear(CosmosClient.databaseAccount().id);
|
this.container.mostRecentActivity.clear(userContext.databaseAccount?.id);
|
||||||
this.forceRender();
|
this.forceRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,7 +194,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createRecentItems(): SplashScreenItem[] {
|
private createRecentItems(): SplashScreenItem[] {
|
||||||
return this.container.mostRecentActivity.getItems(CosmosClient.databaseAccount().id).map(item => ({
|
return this.container.mostRecentActivity.getItems(userContext.databaseAccount?.id).map(item => ({
|
||||||
iconSrc: MostRecentActivity.MostRecentActivity.getItemIcon(item),
|
iconSrc: MostRecentActivity.MostRecentActivity.getItemIcon(item),
|
||||||
title: item.title,
|
title: item.title,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
|
|
||||||
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
@@ -14,12 +13,13 @@ import SaveIcon from "../../../images/save-cosmos.svg";
|
|||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { updateOfferThroughputBeyondLimit, updateOffer } from "../../Common/DocumentClientUtilityBase";
|
import { updateOffer } from "../../Common/DocumentClientUtilityBase";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||||
|
|
||||||
const updateThroughputBeyondLimitWarningMessage: string = `
|
const updateThroughputBeyondLimitWarningMessage: string = `
|
||||||
You are about to request an increase in throughput beyond the pre-allocated capacity.
|
You are about to request an increase in throughput beyond the pre-allocated capacity.
|
||||||
@@ -520,16 +520,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||||
) {
|
) {
|
||||||
const requestPayload: DataModels.UpdateOfferThroughputRequest = {
|
const requestPayload = {
|
||||||
subscriptionId: CosmosClient.subscriptionId(),
|
subscriptionId: userContext.subscriptionId,
|
||||||
databaseAccountName: CosmosClient.databaseAccount().name,
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
resourceGroup: CosmosClient.resourceGroup(),
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseName: this.database.id(),
|
databaseName: this.database.id(),
|
||||||
collectionName: undefined,
|
|
||||||
throughput: newThroughput,
|
throughput: newThroughput,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
};
|
};
|
||||||
const updateOfferBeyondLimitPromise: Q.Promise<void> = updateOfferThroughputBeyondLimit(requestPayload).then(
|
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
|
||||||
() => {
|
() => {
|
||||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||||
this.throughput(originalThroughputValue);
|
this.throughput(originalThroughputValue);
|
||||||
@@ -553,7 +552,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
promises.push(updateOfferBeyondLimitPromise);
|
promises.push(Q(updateOfferBeyondLimitPromise));
|
||||||
} else {
|
} else {
|
||||||
const newOffer: DataModels.Offer = {
|
const newOffer: DataModels.Offer = {
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import TabsBase from "./TabsBase";
|
|||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import { HashMap } from "../../Common/HashMap";
|
import { HashMap } from "../../Common/HashMap";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class MongoShellTab extends TabsBase {
|
export default class MongoShellTab extends TabsBase {
|
||||||
public url: ko.Computed<string>;
|
public url: ko.Computed<string>;
|
||||||
@@ -26,8 +26,8 @@ export default class MongoShellTab extends TabsBase {
|
|||||||
this._logTraces = new HashMap<number>();
|
this._logTraces = new HashMap<number>();
|
||||||
this._container = options.collection.container;
|
this._container = options.collection.container;
|
||||||
this.url = ko.computed<string>(() => {
|
this.url = ko.computed<string>(() => {
|
||||||
const account = CosmosClient.databaseAccount();
|
const account = userContext.databaseAccount;
|
||||||
const resourceId: string = account && account.id;
|
const resourceId = account && account.id;
|
||||||
const accountName = account && account.name;
|
const accountName = account && account.name;
|
||||||
const mongoEndpoint = account && (account.properties.mongoEndpoint || account.properties.documentEndpoint);
|
const mongoEndpoint = account && (account.properties.mongoEndpoint || account.properties.documentEndpoint);
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export default class MongoShellTab extends TabsBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorization: string = CosmosClient.authorizationToken() || "";
|
const authorization: string = userContext.authorizationToken || "";
|
||||||
const resourceId = this._container.databaseAccount().id;
|
const resourceId = this._container.databaseAccount().id;
|
||||||
const accountName = this._container.databaseAccount().name;
|
const accountName = this._container.databaseAccount().name;
|
||||||
const documentEndpoint =
|
const documentEndpoint =
|
||||||
@@ -111,10 +111,10 @@ export default class MongoShellTab extends TabsBase {
|
|||||||
const collectionId = this.collection.id();
|
const collectionId = this.collection.id();
|
||||||
const apiEndpoint = EnvironmentUtility.getMongoBackendEndpoint(
|
const apiEndpoint = EnvironmentUtility.getMongoBackendEndpoint(
|
||||||
this._container.serverId(),
|
this._container.serverId(),
|
||||||
CosmosClient.databaseAccount().location,
|
userContext.databaseAccount.location,
|
||||||
this._container.extensionEndpoint()
|
this._container.extensionEndpoint()
|
||||||
).replace("/api/mongo/explorer", "");
|
).replace("/api/mongo/explorer", "");
|
||||||
const encryptedAuthToken: string = CosmosClient.accessToken();
|
const encryptedAuthToken: string = userContext.accessToken;
|
||||||
|
|
||||||
shellIframe.contentWindow.postMessage(
|
shellIframe.contentWindow.postMessage(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
|
|||||||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||||
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
||||||
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||||
import { config } from "../../Config";
|
import { configContext } from "../../ConfigContext";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
||||||
|
|
||||||
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
||||||
account: DataModels.DatabaseAccount;
|
account: DataModels.DatabaseAccount;
|
||||||
@@ -122,6 +123,7 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||||
|
|
||||||
const saveLabel = "Save";
|
const saveLabel = "Save";
|
||||||
|
const copyToLabel = "Copy to ...";
|
||||||
const publishLabel = "Publish to gallery";
|
const publishLabel = "Publish to gallery";
|
||||||
const workspaceLabel = "No Workspace";
|
const workspaceLabel = "No Workspace";
|
||||||
const kernelLabel = "No Kernel";
|
const kernelLabel = "No Kernel";
|
||||||
@@ -145,6 +147,30 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
const cellCodeType = "code";
|
const cellCodeType = "code";
|
||||||
const cellMarkdownType = "markdown";
|
const cellMarkdownType = "markdown";
|
||||||
const cellRawType = "raw";
|
const cellRawType = "raw";
|
||||||
|
|
||||||
|
const saveButtonChildren = [];
|
||||||
|
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
|
saveButtonChildren.push({
|
||||||
|
iconName: "Copy",
|
||||||
|
onCommandClick: () => this.copyNotebook(),
|
||||||
|
commandButtonLabel: copyToLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: copyToLabel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.container.isGalleryPublishEnabled()) {
|
||||||
|
saveButtonChildren.push({
|
||||||
|
iconName: "PublishContent",
|
||||||
|
onCommandClick: async () => await this.publishToGallery(),
|
||||||
|
commandButtonLabel: publishLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: publishLabel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let buttons: CommandButtonComponentProps[] = [
|
let buttons: CommandButtonComponentProps[] = [
|
||||||
{
|
{
|
||||||
iconSrc: SaveIcon,
|
iconSrc: SaveIcon,
|
||||||
@@ -154,26 +180,17 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
ariaLabel: saveLabel,
|
ariaLabel: saveLabel,
|
||||||
children: this.container.isGalleryPublishEnabled()
|
children: saveButtonChildren.length && [
|
||||||
? [
|
{
|
||||||
{
|
iconName: "Save",
|
||||||
iconName: "Save",
|
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
commandButtonLabel: saveLabel,
|
||||||
commandButtonLabel: saveLabel,
|
hasPopup: false,
|
||||||
hasPopup: false,
|
disabled: false,
|
||||||
disabled: false,
|
ariaLabel: saveLabel
|
||||||
ariaLabel: saveLabel
|
},
|
||||||
},
|
...saveButtonChildren
|
||||||
{
|
]
|
||||||
iconName: "PublishContent",
|
|
||||||
onCommandClick: () => this.publishToGallery(),
|
|
||||||
commandButtonLabel: publishLabel,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: false,
|
|
||||||
ariaLabel: publishLabel
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: undefined
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
@@ -423,7 +440,7 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
password: undefined,
|
password: undefined,
|
||||||
endpoints: [
|
endpoints: [
|
||||||
{
|
{
|
||||||
endpoint: `https://${workspace.name}.${config.ARCADIA_LIVY_ENDPOINT_DNS_ZONE}/livyApi/versions/${ArmApiVersions.arcadiaLivy}/sparkPools/${selectedPool.name}/`,
|
endpoint: `https://${workspace.name}.${configContext.ARCADIA_LIVY_ENDPOINT_DNS_ZONE}/livyApi/versions/${ArmApiVersions.arcadiaLivy}/sparkPools/${selectedPool.name}/`,
|
||||||
kind: DataModels.SparkClusterEndpointKind.Livy
|
kind: DataModels.SparkClusterEndpointKind.Livy
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -456,15 +473,27 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private publishToGallery = () => {
|
private publishToGallery = async () => {
|
||||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
this.container.publishNotebook(
|
await this.container.publishNotebook(
|
||||||
notebookContent.name,
|
notebookContent.name,
|
||||||
notebookContent.content,
|
notebookContent.content,
|
||||||
this.notebookComponentAdapter.getNotebookParentElement()
|
this.notebookComponentAdapter.getNotebookParentElement()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private copyNotebook = () => {
|
||||||
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
|
let content: string;
|
||||||
|
if (typeof notebookContent.content === "string") {
|
||||||
|
content = notebookContent.content;
|
||||||
|
} else {
|
||||||
|
content = stringifyNotebook(toJS(notebookContent.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.copyNotebook(notebookContent.name, content);
|
||||||
|
};
|
||||||
|
|
||||||
private traceTelemetry(actionType: number) {
|
private traceTelemetry(actionType: number) {
|
||||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Database from "../Tree/Database";
|
|||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import SettingsTab from "../Tabs/SettingsTab";
|
import SettingsTab from "../Tabs/SettingsTab";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { IndexingPolicies } from "../../Shared/Constants";
|
||||||
|
|
||||||
describe("Settings tab", () => {
|
describe("Settings tab", () => {
|
||||||
const baseCollection: DataModels.Collection = {
|
const baseCollection: DataModels.Collection = {
|
||||||
@@ -16,7 +17,7 @@ describe("Settings tab", () => {
|
|||||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||||
conflictResolutionPath: "/_ts"
|
conflictResolutionPath: "/_ts"
|
||||||
},
|
},
|
||||||
indexingPolicy: {},
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
_rid: "",
|
_rid: "",
|
||||||
_self: "",
|
_self: "",
|
||||||
_etag: "",
|
_etag: "",
|
||||||
@@ -51,7 +52,7 @@ describe("Settings tab", () => {
|
|||||||
defaultTtl: 200,
|
defaultTtl: 200,
|
||||||
partitionKey: null,
|
partitionKey: null,
|
||||||
conflictResolutionPolicy: null,
|
conflictResolutionPolicy: null,
|
||||||
indexingPolicy: {},
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
_rid: "",
|
_rid: "",
|
||||||
_self: "",
|
_self: "",
|
||||||
_etag: "",
|
_etag: "",
|
||||||
@@ -345,7 +346,6 @@ describe("Settings tab", () => {
|
|||||||
|
|
||||||
const offer: DataModels.Offer = null;
|
const offer: DataModels.Offer = null;
|
||||||
const defaultTtl = 200;
|
const defaultTtl = 200;
|
||||||
const indexingPolicy = {};
|
|
||||||
const database = new Database(explorer, baseDatabase, null);
|
const database = new Database(explorer, baseDatabase, null);
|
||||||
const conflictResolutionPolicy = {
|
const conflictResolutionPolicy = {
|
||||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||||
@@ -367,7 +367,7 @@ describe("Settings tab", () => {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
conflictResolutionPolicy: conflictResolutionPolicy,
|
conflictResolutionPolicy: conflictResolutionPolicy,
|
||||||
indexingPolicy: indexingPolicy,
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
_rid: "",
|
_rid: "",
|
||||||
_self: "",
|
_self: "",
|
||||||
_etag: "",
|
_etag: "",
|
||||||
|
|||||||
@@ -14,16 +14,14 @@ import SaveIcon from "../../../images/save-cosmos.svg";
|
|||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import {
|
import { updateOffer } from "../../Common/DocumentClientUtilityBase";
|
||||||
updateOfferThroughputBeyondLimit,
|
import { updateCollection } from "../../Common/dataAccess/updateCollection";
|
||||||
updateOffer,
|
|
||||||
updateCollection
|
|
||||||
} from "../../Common/DocumentClientUtilityBase";
|
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||||
|
|
||||||
const ttlWarning: string = `
|
const ttlWarning: string = `
|
||||||
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application.
|
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application.
|
||||||
@@ -347,7 +345,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
if (!this.isAutoPilotSelected()) {
|
if (!this.isAutoPilotSelected()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const originalAutoPilotSettings = this.collection.offer().content.offerAutopilotSettings;
|
const originalAutoPilotSettings = this.collection?.offer()?.content?.offerAutopilotSettings;
|
||||||
if (!originalAutoPilotSettings) {
|
if (!originalAutoPilotSettings) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1012,8 +1010,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSaveClick = (): Q.Promise<any> => {
|
public onSaveClick = async (): Promise<any> => {
|
||||||
let promises: Q.Promise<void>[] = [];
|
|
||||||
this.isExecutionError(false);
|
this.isExecutionError(false);
|
||||||
|
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
@@ -1026,50 +1023,60 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
|
|
||||||
const newCollectionAttributes: any = {};
|
const newCollectionAttributes: any = {};
|
||||||
|
|
||||||
if (this.shouldUpdateCollection()) {
|
try {
|
||||||
let defaultTtl: number;
|
if (this.shouldUpdateCollection()) {
|
||||||
switch (this.timeToLive()) {
|
let defaultTtl: number;
|
||||||
case "on":
|
switch (this.timeToLive()) {
|
||||||
defaultTtl = Number(this.timeToLiveSeconds());
|
case "on":
|
||||||
break;
|
defaultTtl = Number(this.timeToLiveSeconds());
|
||||||
case "on-nodefault":
|
break;
|
||||||
defaultTtl = -1;
|
case "on-nodefault":
|
||||||
break;
|
defaultTtl = -1;
|
||||||
case "off":
|
break;
|
||||||
default:
|
case "off":
|
||||||
defaultTtl = undefined;
|
default:
|
||||||
break;
|
defaultTtl = undefined;
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
newCollectionAttributes.defaultTtl = defaultTtl;
|
newCollectionAttributes.defaultTtl = defaultTtl;
|
||||||
|
|
||||||
newCollectionAttributes.indexingPolicy = this.indexingPolicyContent();
|
newCollectionAttributes.indexingPolicy = this.indexingPolicyContent();
|
||||||
|
|
||||||
newCollectionAttributes.changeFeedPolicy =
|
newCollectionAttributes.changeFeedPolicy =
|
||||||
this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On
|
this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On
|
||||||
? ({
|
? ({
|
||||||
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
|
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
|
||||||
} as DataModels.ChangeFeedPolicy)
|
} as DataModels.ChangeFeedPolicy)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
|
||||||
|
? this.analyticalStorageTtlSelection() === "on"
|
||||||
|
? Number(this.analyticalStorageTtlSeconds())
|
||||||
|
: Constants.AnalyticalStorageTtl.Infinite
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
|
newCollectionAttributes.geospatialConfig = {
|
||||||
? this.analyticalStorageTtlSelection() === "on"
|
type: this.geospatialConfigType()
|
||||||
? Number(this.analyticalStorageTtlSeconds())
|
};
|
||||||
: Constants.AnalyticalStorageTtl.Infinite
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
newCollectionAttributes.geospatialConfig = {
|
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
||||||
type: this.geospatialConfigType()
|
if (!!conflictResolutionChanges) {
|
||||||
};
|
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
|
||||||
|
}
|
||||||
|
|
||||||
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
const newCollection: DataModels.Collection = _.extend(
|
||||||
if (!!conflictResolutionChanges) {
|
{},
|
||||||
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
|
this.collection.rawDataModel,
|
||||||
}
|
newCollectionAttributes
|
||||||
|
);
|
||||||
|
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id(),
|
||||||
|
newCollection
|
||||||
|
);
|
||||||
|
|
||||||
const newCollection: DataModels.Collection = _.extend({}, this.collection.rawDataModel, newCollectionAttributes);
|
if (updatedCollection) {
|
||||||
const updateCollectionPromise = updateCollection(this.collection.databaseId, this.collection, newCollection).then(
|
|
||||||
(updatedCollection: DataModels.Collection) => {
|
|
||||||
this.collection.rawDataModel = updatedCollection;
|
this.collection.rawDataModel = updatedCollection;
|
||||||
this.collection.defaultTtl(updatedCollection.defaultTtl);
|
this.collection.defaultTtl(updatedCollection.defaultTtl);
|
||||||
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
|
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
|
||||||
@@ -1079,164 +1086,133 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
promises.push(updateCollectionPromise);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.throughput.editableIsDirty() ||
|
|
||||||
this.rupm.editableIsDirty() ||
|
|
||||||
this._isAutoPilotDirty() ||
|
|
||||||
this._hasProvisioningTypeChanged()
|
|
||||||
) {
|
|
||||||
const newThroughput = this.throughput();
|
|
||||||
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
|
||||||
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
|
|
||||||
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
|
|
||||||
|
|
||||||
if (newOffer.content) {
|
|
||||||
newOffer.content.offerThroughput = newThroughput;
|
|
||||||
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
|
|
||||||
} else {
|
|
||||||
newOffer = _.extend({}, newOffer, {
|
|
||||||
content: {
|
|
||||||
offerThroughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
|
||||||
|
|
||||||
if (this.isAutoPilotSelected()) {
|
|
||||||
if (!this.hasAutoPilotV2FeatureFlag()) {
|
|
||||||
newOffer.content.offerAutopilotSettings = {
|
|
||||||
maxThroughput: this.autoPilotThroughput()
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
newOffer.content.offerAutopilotSettings = {
|
|
||||||
tier: this.selectedAutoPilotTier()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// user has changed from provisioned --> autoscale
|
|
||||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerThroughput;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.isAutoPilotSelected(false);
|
|
||||||
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
|
|
||||||
|
|
||||||
// user has changed from autoscale --> provisioned
|
|
||||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.throughput.editableIsDirty() ||
|
||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.rupm.editableIsDirty() ||
|
||||||
this.container != null
|
this._isAutoPilotDirty() ||
|
||||||
|
this._hasProvisioningTypeChanged()
|
||||||
) {
|
) {
|
||||||
const requestPayload: DataModels.UpdateOfferThroughputRequest = {
|
const newThroughput = this.throughput();
|
||||||
subscriptionId: CosmosClient.subscriptionId(),
|
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||||
databaseAccountName: CosmosClient.databaseAccount().name,
|
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
|
||||||
resourceGroup: CosmosClient.resourceGroup(),
|
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
|
||||||
};
|
|
||||||
const updateOfferBeyondLimitPromise: Q.Promise<void> = updateOfferThroughputBeyondLimit(requestPayload).then(
|
|
||||||
() => {
|
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.throughput(originalThroughputValue);
|
|
||||||
this.notificationStatusInfo(
|
|
||||||
throughputApplyDelayedMessage(
|
|
||||||
this.isAutoPilotSelected(),
|
|
||||||
originalThroughputValue,
|
|
||||||
this._getThroughputUnit(),
|
|
||||||
this.collection.databaseId,
|
|
||||||
this.collection.id(),
|
|
||||||
newThroughput
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.throughput.valueHasMutated(); // force component re-render
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.UpdateSettings,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
databaseName: this.collection && this.collection.databaseId,
|
|
||||||
collectionName: this.collection && this.collection.id(),
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
error: error
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
promises.push(updateOfferBeyondLimitPromise);
|
|
||||||
} else {
|
|
||||||
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
|
|
||||||
(updatedOffer: DataModels.Offer) => {
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.collection.offer.valueHasMutated();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
promises.push(updateOfferPromise);
|
if (newOffer.content) {
|
||||||
}
|
newOffer.content.offerThroughput = newThroughput;
|
||||||
}
|
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
|
||||||
|
} else {
|
||||||
if (promises.length === 0) {
|
newOffer = _.extend({}, newOffer, {
|
||||||
this.isExecuting(false);
|
content: {
|
||||||
}
|
offerThroughput: newThroughput,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||||
return Q.all(promises)
|
}
|
||||||
.then(
|
});
|
||||||
() => {
|
|
||||||
this.container.isRefreshingExplorer(false);
|
|
||||||
this._setBaseline();
|
|
||||||
this.collection.readSettings();
|
|
||||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.UpdateSettings,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle()
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(reason: any) => {
|
|
||||||
this.container.isRefreshingExplorer(false);
|
|
||||||
this.isExecutionError(true);
|
|
||||||
console.error(reason);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.UpdateSettings,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle()
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.finally(() => this.isExecuting(false));
|
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||||
|
|
||||||
|
if (this.isAutoPilotSelected()) {
|
||||||
|
if (!this.hasAutoPilotV2FeatureFlag()) {
|
||||||
|
newOffer.content.offerAutopilotSettings = {
|
||||||
|
maxThroughput: this.autoPilotThroughput()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newOffer.content.offerAutopilotSettings = {
|
||||||
|
tier: this.selectedAutoPilotTier()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// user has changed from provisioned --> autoscale
|
||||||
|
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||||
|
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||||
|
delete newOffer.content.offerAutopilotSettings;
|
||||||
|
} else {
|
||||||
|
delete newOffer.content.offerThroughput;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isAutoPilotSelected(false);
|
||||||
|
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
|
||||||
|
|
||||||
|
// user has changed from autoscale --> provisioned
|
||||||
|
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||||
|
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||||
|
} else {
|
||||||
|
delete newOffer.content.offerAutopilotSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
this.container != null
|
||||||
|
) {
|
||||||
|
const requestPayload = {
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
databaseName: this.collection.databaseId,
|
||||||
|
collectionName: this.collection.id(),
|
||||||
|
throughput: newThroughput,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||||
|
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||||
|
this.throughput(originalThroughputValue);
|
||||||
|
this.notificationStatusInfo(
|
||||||
|
throughputApplyDelayedMessage(
|
||||||
|
this.isAutoPilotSelected(),
|
||||||
|
originalThroughputValue,
|
||||||
|
this._getThroughputUnit(),
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id(),
|
||||||
|
newThroughput
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.throughput.valueHasMutated(); // force component re-render
|
||||||
|
} else {
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.collection.offer.valueHasMutated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.isRefreshingExplorer(false);
|
||||||
|
this._setBaseline();
|
||||||
|
this.collection.readSettings();
|
||||||
|
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||||
|
TelemetryProcessor.traceSuccess(
|
||||||
|
Action.UpdateSettings,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.tabTitle()
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.container.isRefreshingExplorer(false);
|
||||||
|
this.isExecutionError(true);
|
||||||
|
console.error(error);
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.UpdateSettings,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
databaseName: this.collection && this.collection.databaseId,
|
||||||
|
collectionName: this.collection && this.collection.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.tabTitle(),
|
||||||
|
error: error
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isExecuting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onRevertClick = (): Q.Promise<any> => {
|
public onRevertClick = (): Q.Promise<any> => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as _ from "underscore";
|
|||||||
import UploadWorker from "worker-loader!../../workers/upload";
|
import UploadWorker from "worker-loader!../../workers/upload";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -31,7 +30,7 @@ import SettingsTab from "../Tabs/SettingsTab";
|
|||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import UserDefinedFunction from "./UserDefinedFunction";
|
import UserDefinedFunction from "./UserDefinedFunction";
|
||||||
import { config } from "../../Config";
|
import { configContext } from "../../ConfigContext";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import {
|
import {
|
||||||
createDocument,
|
createDocument,
|
||||||
@@ -42,6 +41,7 @@ import {
|
|||||||
readOffer,
|
readOffer,
|
||||||
readOffers
|
readOffers
|
||||||
} from "../../Common/DocumentClientUtilityBase";
|
} from "../../Common/DocumentClientUtilityBase";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class Collection implements ViewModels.Collection {
|
export default class Collection implements ViewModels.Collection {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
@@ -472,7 +472,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
graphTab = new GraphTab({
|
graphTab = new GraphTab({
|
||||||
account: CosmosClient.databaseAccount(),
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.Graph,
|
tabKind: ViewModels.CollectionTabKind.Graph,
|
||||||
node: this,
|
node: this,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -480,7 +480,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
collection: this,
|
collection: this,
|
||||||
selfLink: this.self,
|
selfLink: this.self,
|
||||||
masterKey: CosmosClient.masterKey() || "",
|
masterKey: userContext.masterKey || "",
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
@@ -648,7 +648,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
// TODO: Use the collection entity cache to get quota info
|
// TODO: Use the collection entity cache to get quota info
|
||||||
const quotaInfoPromise: Q.Promise<DataModels.CollectionQuotaInfo> = readCollectionQuotaInfo(this);
|
const quotaInfoPromise: Q.Promise<DataModels.CollectionQuotaInfo> = readCollectionQuotaInfo(this);
|
||||||
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers();
|
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
|
||||||
|
isServerless: this.container.isServerlessEnabled()
|
||||||
|
});
|
||||||
Q.all([quotaInfoPromise, offerInfoPromise]).then(
|
Q.all([quotaInfoPromise, offerInfoPromise]).then(
|
||||||
() => {
|
() => {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
@@ -657,9 +659,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
|
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
|
||||||
|
|
||||||
const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel);
|
const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel);
|
||||||
const isDatabaseShared = this.getDatabase() && this.getDatabase().isDatabaseShared();
|
if (!collectionOffer) {
|
||||||
const isServerless = this.container.isServerlessEnabled();
|
|
||||||
if ((isDatabaseShared || isServerless) && !collectionOffer) {
|
|
||||||
this.quotaInfo(quotaInfo);
|
this.quotaInfo(quotaInfo);
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadOffers,
|
Action.LoadOffers,
|
||||||
@@ -804,14 +804,14 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const graphTab: GraphTab = new GraphTab({
|
const graphTab: GraphTab = new GraphTab({
|
||||||
account: CosmosClient.databaseAccount(),
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.Graph,
|
tabKind: ViewModels.CollectionTabKind.Graph,
|
||||||
node: this,
|
node: this,
|
||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
collection: this,
|
collection: this,
|
||||||
selfLink: this.self,
|
selfLink: this.self,
|
||||||
masterKey: CosmosClient.masterKey() || "",
|
masterKey: userContext.masterKey || "",
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
@@ -1181,11 +1181,11 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
documentClientParams: {
|
documentClientParams: {
|
||||||
databaseId: this.databaseId,
|
databaseId: this.databaseId,
|
||||||
containerId: this.id(),
|
containerId: this.id(),
|
||||||
masterKey: CosmosClient.masterKey(),
|
masterKey: userContext.masterKey,
|
||||||
endpoint: CosmosClient.endpoint(),
|
endpoint: userContext.endpoint,
|
||||||
accessToken: CosmosClient.accessToken(),
|
accessToken: userContext.accessToken,
|
||||||
platform: config.platform,
|
platform: configContext.platform,
|
||||||
databaseAccount: CosmosClient.databaseAccount()
|
databaseAccount: userContext.databaseAccount
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
|
|||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { readCollections, readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
|
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
|
||||||
|
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||||
|
|
||||||
export default class Database implements ViewModels.Database {
|
export default class Database implements ViewModels.Database {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
@@ -122,10 +123,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
|
|
||||||
public readSettings(): Q.Promise<void> {
|
public readSettings(): Q.Promise<void> {
|
||||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
const deferred: Q.Deferred<void> = Q.defer<void>();
|
||||||
if (this.container.isServerlessEnabled()) {
|
|
||||||
deferred.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.container.isRefreshingExplorer(true);
|
this.container.isRefreshingExplorer(true);
|
||||||
const databaseDataModel: DataModels.Database = <DataModels.Database>{
|
const databaseDataModel: DataModels.Database = <DataModels.Database>{
|
||||||
id: this.id(),
|
id: this.id(),
|
||||||
@@ -137,7 +134,9 @@ export default class Database implements ViewModels.Database {
|
|||||||
defaultExperience: this.container.defaultExperience()
|
defaultExperience: this.container.defaultExperience()
|
||||||
});
|
});
|
||||||
|
|
||||||
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers();
|
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
|
||||||
|
isServerless: this.container.isServerlessEnabled()
|
||||||
|
});
|
||||||
Q.all([offerInfoPromise]).then(
|
Q.all([offerInfoPromise]).then(
|
||||||
() => {
|
() => {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
@@ -146,6 +145,11 @@ export default class Database implements ViewModels.Database {
|
|||||||
offerInfoPromise.valueOf(),
|
offerInfoPromise.valueOf(),
|
||||||
databaseDataModel
|
databaseDataModel
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!databaseOffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
|
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
|
||||||
const offerThroughputInfo: DataModels.OfferThroughputInfo = {
|
const offerThroughputInfo: DataModels.OfferThroughputInfo = {
|
||||||
minimumRUForCollection:
|
minimumRUForCollection:
|
||||||
@@ -259,7 +263,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
let collectionVMs: Collection[] = [];
|
let collectionVMs: Collection[] = [];
|
||||||
let deferred: Q.Deferred<void> = Q.defer<void>();
|
let deferred: Q.Deferred<void> = Q.defer<void>();
|
||||||
|
|
||||||
readCollections(this).then(
|
readCollections(this.id()).then(
|
||||||
(collections: DataModels.Collection[]) => {
|
(collections: DataModels.Collection[]) => {
|
||||||
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
|
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
|
||||||
let deltaCollections = this.getDeltaCollections(collections);
|
let deltaCollections = this.getDeltaCollections(collections);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import DeleteIcon from "../../../images/delete.svg";
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
@@ -31,8 +31,12 @@ import UserDefinedFunction from "./UserDefinedFunction";
|
|||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
import Trigger from "./Trigger";
|
import Trigger from "./Trigger";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
|
public static readonly MyNotebooksTitle = "My Notebooks";
|
||||||
|
public static readonly GitHubReposTitle = "GitHub repos";
|
||||||
|
|
||||||
private static readonly DataTitle = "DATA";
|
private static readonly DataTitle = "DATA";
|
||||||
private static readonly NotebooksTitle = "NOTEBOOKS";
|
private static readonly NotebooksTitle = "NOTEBOOKS";
|
||||||
private static readonly PseudoDirPath = "PsuedoDir";
|
private static readonly PseudoDirPath = "PsuedoDir";
|
||||||
@@ -104,7 +108,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.myNotebooksContentRoot = {
|
this.myNotebooksContentRoot = {
|
||||||
name: "My Notebooks",
|
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
path: this.container.getNotebookBasePath(),
|
path: this.container.getNotebookBasePath(),
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
@@ -118,7 +122,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
this.gitHubNotebooksContentRoot = {
|
this.gitHubNotebooksContentRoot = {
|
||||||
name: "GitHub repos",
|
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
path: ResourceTreeAdapter.PseudoDirPath,
|
path: ResourceTreeAdapter.PseudoDirPath,
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
@@ -224,7 +228,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
this.container.mostRecentActivity.addItem(CosmosClient.databaseAccount().id, {
|
this.container.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
|
||||||
type: MostRecentActivity.Type.OpenCollection,
|
type: MostRecentActivity.Type.OpenCollection,
|
||||||
title: collection.id(),
|
title: collection.id(),
|
||||||
description: "Data",
|
description: "Data",
|
||||||
@@ -490,7 +494,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private pushItemToMostRecent(item: NotebookContentItem) {
|
private pushItemToMostRecent(item: NotebookContentItem) {
|
||||||
this.container.mostRecentActivity.addItem(CosmosClient.databaseAccount().id, {
|
this.container.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
|
||||||
type: MostRecentActivity.Type.OpenNotebook,
|
type: MostRecentActivity.Type.OpenNotebook,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
description: "Notebook",
|
description: "Notebook",
|
||||||
@@ -542,38 +546,59 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
(activeTab as any).notebookPath() === item.path
|
(activeTab as any).notebookPath() === item.path
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextMenu: createFileContextMenu
|
contextMenu: createFileContextMenu && this.createFileContextMenu(item),
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: "Rename",
|
|
||||||
iconSrc: NotebookIcon,
|
|
||||||
onClick: () => this.container.renameNotebook(item)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Delete",
|
|
||||||
iconSrc: DeleteIcon,
|
|
||||||
onClick: () => {
|
|
||||||
this.container.showOkCancelModalDialog(
|
|
||||||
"Confirm delete",
|
|
||||||
`Are you sure you want to delete "${item.name}"`,
|
|
||||||
"Delete",
|
|
||||||
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
|
|
||||||
"Cancel",
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Download",
|
|
||||||
iconSrc: NotebookIcon,
|
|
||||||
onClick: () => this.container.downloadFile(item)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: undefined,
|
|
||||||
data: item
|
data: item
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
||||||
|
let items: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => this.container.renameNotebook(item)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
iconSrc: DeleteIcon,
|
||||||
|
onClick: () => {
|
||||||
|
this.container.showOkCancelModalDialog(
|
||||||
|
"Confirm delete",
|
||||||
|
`Are you sure you want to delete "${item.name}"`,
|
||||||
|
"Delete",
|
||||||
|
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
|
||||||
|
"Cancel",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Copy to ...",
|
||||||
|
iconSrc: CopyIcon,
|
||||||
|
onClick: () => this.copyNotebook(item)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Download",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => this.container.downloadFile(item)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// "Copy to ..." isn't needed if github locations are not available
|
||||||
|
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
|
items = items.filter(item => item.label !== "Copy to ...");
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private copyNotebook = async (item: NotebookContentItem) => {
|
||||||
|
const content = await this.container.readFile(item);
|
||||||
|
if (content) {
|
||||||
|
this.container.copyNotebook(item.name, content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
||||||
let items: TreeNodeMenuItem[] = [
|
let items: TreeNodeMenuItem[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import * as ko from "knockout";
|
|||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
|
export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
|
||||||
public parameters: ko.Observable<number>;
|
public parameters: ko.Observable<number>;
|
||||||
@@ -44,7 +44,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.onDocumentDBDocumentsClick();
|
collection.onDocumentDBDocumentsClick();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
this.container.mostRecentActivity.addItem(CosmosClient.databaseAccount().id, {
|
this.container.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
|
||||||
type: MostRecentActivity.Type.OpenCollection,
|
type: MostRecentActivity.Type.OpenCollection,
|
||||||
title: collection.id(),
|
title: collection.id(),
|
||||||
description: "Data",
|
description: "Data",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
|||||||
import { Text, Link } from "office-ui-fabric-react";
|
import { Text, Link } from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import { initializeConfiguration } from "../Config";
|
import { initializeConfiguration } from "../ConfigContext";
|
||||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||||
import {
|
import {
|
||||||
GalleryAndNotebookViewerComponent,
|
GalleryAndNotebookViewerComponent,
|
||||||
|
|||||||
@@ -317,6 +317,13 @@ export class GitHubClient {
|
|||||||
objectExpression: `refs/heads/${branch}:${path || ""}`
|
objectExpression: `refs/heads/${branch}:${path || ""}`
|
||||||
} as ContentsQueryParams)) as ContentsQueryResponse;
|
} as ContentsQueryParams)) as ContentsQueryResponse;
|
||||||
|
|
||||||
|
if (!response.repository.object) {
|
||||||
|
return {
|
||||||
|
status: HttpStatusCodes.NotFound,
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let data: IGitHubFile | IGitHubFile[];
|
let data: IGitHubFile | IGitHubFile[];
|
||||||
const entries = response.repository.object.entries;
|
const entries = response.repository.object.entries;
|
||||||
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
|
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/
|
|||||||
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
||||||
import { from, Observable, of } from "rxjs";
|
import { from, Observable, of } from "rxjs";
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
import { AjaxResponse } from "rxjs/ajax";
|
||||||
|
import * as Base64Utils from "../Utils/Base64Utils";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||||
import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit, IGitHubBranch } from "./GitHubClient";
|
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
||||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||||
import UrlUtility from "../Common/UrlUtility";
|
import UrlUtility from "../Common/UrlUtility";
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ export class GitHubContentProvider implements IContentProvider {
|
|||||||
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = btoa(stringifyNotebook(toJS(makeNotebookRecord())));
|
const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord())));
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
@@ -195,34 +196,63 @@ export class GitHubContentProvider implements IContentProvider {
|
|||||||
return from(
|
return from(
|
||||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||||
try {
|
try {
|
||||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
let commitMsg: string;
|
||||||
|
if (content.status === HttpStatusCodes.NotFound) {
|
||||||
|
// We'll create a new file since it doesn't exist
|
||||||
|
commitMsg = await this.params.promptForCommitMsg("Save", "Save");
|
||||||
|
if (!commitMsg) {
|
||||||
|
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
||||||
|
}
|
||||||
|
|
||||||
let updatedContent: string;
|
let updatedContent: string;
|
||||||
if (model.type === "notebook") {
|
if (model.type === "notebook") {
|
||||||
updatedContent = btoa(stringifyNotebook(model.content as Notebook));
|
updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook));
|
||||||
} else if (model.type === "file") {
|
} else if (model.type === "file") {
|
||||||
updatedContent = model.content as string;
|
updatedContent = model.content as string;
|
||||||
if (model.format !== "base64") {
|
if (model.format !== "base64") {
|
||||||
updatedContent = btoa(updatedContent);
|
updatedContent = Base64Utils.utf8ToB64(updatedContent);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new GitHubContentProviderError("Unsupported content type");
|
throw new GitHubContentProviderError("Unsupported content type");
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitHubFile = content.data as IGitHubFile;
|
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
let gitHubFile: IGitHubFile;
|
||||||
gitHubFile.repo.owner,
|
if (content.data) {
|
||||||
gitHubFile.repo.name,
|
gitHubFile = content.data as IGitHubFile;
|
||||||
gitHubFile.branch.name,
|
|
||||||
gitHubFile.path,
|
|
||||||
commitMsg,
|
|
||||||
updatedContent,
|
|
||||||
gitHubFile.sha
|
|
||||||
);
|
|
||||||
if (response.status !== HttpStatusCodes.OK) {
|
|
||||||
throw new GitHubContentProviderError("Failed to update", response.status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gitHubFile.commit = response.data;
|
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
contentInfo.path,
|
||||||
|
commitMsg,
|
||||||
|
updatedContent,
|
||||||
|
gitHubFile?.sha
|
||||||
|
);
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) {
|
||||||
|
throw new GitHubContentProviderError("Failed to create or update", response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gitHubFile) {
|
||||||
|
gitHubFile.commit = response.data;
|
||||||
|
} else {
|
||||||
|
const contentResponse = await this.params.gitHubClient.getContentsAsync(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
contentInfo.path
|
||||||
|
);
|
||||||
|
if (contentResponse.status !== HttpStatusCodes.OK) {
|
||||||
|
throw new GitHubContentProviderError("Failed to get content", response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
gitHubFile = contentResponse.data as IGitHubFile;
|
||||||
|
}
|
||||||
|
|
||||||
return this.createSuccessAjaxResponse(
|
return this.createSuccessAjaxResponse(
|
||||||
HttpStatusCodes.OK,
|
HttpStatusCodes.OK,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { config } from "../Config";
|
import { configContext } from "../ConfigContext";
|
||||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { JunoClient } from "../Juno/JunoClient";
|
import { JunoClient } from "../Juno/JunoClient";
|
||||||
@@ -55,7 +55,7 @@ export class GitHubOAuthService {
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
scope,
|
scope,
|
||||||
client_id: config.GITHUB_CLIENT_ID,
|
client_id: configContext.GITHUB_CLIENT_ID,
|
||||||
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
|
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
|
||||||
state: this.resetState()
|
state: this.resetState()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ko from "knockout";
|
|||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { IPinnedRepo, JunoClient, IGalleryItem } from "./JunoClient";
|
import { IPinnedRepo, JunoClient, IGalleryItem } from "./JunoClient";
|
||||||
import { config } from "../Config";
|
import { configContext } from "../ConfigContext";
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
|
|
||||||
@@ -47,7 +47,8 @@ const sampleGalleryItems: IGalleryItem[] = [
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ describe("Gallery", () => {
|
|||||||
const response = await junoClient.getSampleNotebooks();
|
const response = await junoClient.getSampleNotebooks();
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/samples`, undefined);
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/samples`, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getPublicNotebooks", async () => {
|
it("getPublicNotebooks", async () => {
|
||||||
@@ -175,7 +176,7 @@ describe("Gallery", () => {
|
|||||||
const response = await junoClient.getPublicNotebooks();
|
const response = await junoClient.getPublicNotebooks();
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/public`, undefined);
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/public`, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getNotebook", async () => {
|
it("getNotebook", async () => {
|
||||||
@@ -185,10 +186,10 @@ describe("Gallery", () => {
|
|||||||
json: () => undefined as any
|
json: () => undefined as any
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await junoClient.getNotebook(id);
|
const response = await junoClient.getNotebookInfo(id);
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`);
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getNotebookContent", async () => {
|
it("getNotebookContent", async () => {
|
||||||
@@ -201,7 +202,7 @@ describe("Gallery", () => {
|
|||||||
const response = await junoClient.getNotebookContent(id);
|
const response = await junoClient.getNotebookContent(id);
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/content`);
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/content`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("increaseNotebookViews", async () => {
|
it("increaseNotebookViews", async () => {
|
||||||
@@ -214,7 +215,7 @@ describe("Gallery", () => {
|
|||||||
const response = await junoClient.increaseNotebookViews(id);
|
const response = await junoClient.increaseNotebookViews(id);
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/views`, {
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/views`, {
|
||||||
method: "PATCH"
|
method: "PATCH"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -231,7 +232,7 @@ describe("Gallery", () => {
|
|||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(
|
expect(window.fetch).toBeCalledWith(
|
||||||
`${config.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/downloads`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/downloads`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -254,7 +255,7 @@ describe("Gallery", () => {
|
|||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(
|
expect(window.fetch).toBeCalledWith(
|
||||||
`${config.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -276,7 +277,7 @@ describe("Gallery", () => {
|
|||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/unfavorite`, {
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/unfavorite`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
@@ -295,7 +296,7 @@ describe("Gallery", () => {
|
|||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, {
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, {
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
@@ -313,7 +314,7 @@ describe("Gallery", () => {
|
|||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/published`, {
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/published`, {
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
@@ -332,7 +333,7 @@ describe("Gallery", () => {
|
|||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`, {
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
@@ -353,24 +354,27 @@ describe("Gallery", () => {
|
|||||||
json: () => undefined as any
|
json: () => undefined as any
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content);
|
const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content, false);
|
||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${config.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery`, {
|
expect(window.fetch).toBeCalledWith(
|
||||||
method: "PUT",
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery`,
|
||||||
headers: {
|
{
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
method: "PUT",
|
||||||
"content-type": "application/json"
|
headers: {
|
||||||
},
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
body: JSON.stringify({
|
"content-type": "application/json"
|
||||||
name,
|
},
|
||||||
description,
|
body: JSON.stringify({
|
||||||
tags,
|
name,
|
||||||
author,
|
description,
|
||||||
thumbnailUrl,
|
tags,
|
||||||
content: JSON.parse(content)
|
author,
|
||||||
})
|
thumbnailUrl,
|
||||||
});
|
content: JSON.parse(content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import { config } from "../Config";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||||
import { IGitHubResponse } from "../GitHub/GitHubClient";
|
import { IGitHubResponse } from "../GitHub/GitHubClient";
|
||||||
@@ -36,6 +36,16 @@ export interface IGalleryItem {
|
|||||||
downloads: number;
|
downloads: number;
|
||||||
favorites: number;
|
favorites: number;
|
||||||
views: number;
|
views: number;
|
||||||
|
newCellId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicGalleryData {
|
||||||
|
metadata: IPublicGalleryMetaData;
|
||||||
|
notebooksData: IGalleryItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicGalleryMetaData {
|
||||||
|
acceptedCodeOfConduct: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserGallery {
|
export interface IUserGallery {
|
||||||
@@ -162,7 +172,62 @@ export class JunoClient {
|
|||||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
// will be renamed once feature.enableCodeOfConduct flag is removed
|
||||||
|
public async fetchPublicNotebooks(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: IPublicGalleryData;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: boolean;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: boolean;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNotebookInfo(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
||||||
|
|
||||||
let data: IGalleryItem;
|
let data: IGalleryItem;
|
||||||
@@ -292,24 +357,37 @@ export class JunoClient {
|
|||||||
tags: string[],
|
tags: string[],
|
||||||
author: string,
|
author: string,
|
||||||
thumbnailUrl: string,
|
thumbnailUrl: string,
|
||||||
content: string
|
content: string,
|
||||||
|
isLinkInjectionEnabled: boolean
|
||||||
): Promise<IJunoResponse<IGalleryItem>> {
|
): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
|
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: isLinkInjectionEnabled
|
||||||
name,
|
? JSON.stringify({
|
||||||
description,
|
name,
|
||||||
tags,
|
description,
|
||||||
author,
|
tags,
|
||||||
thumbnailUrl,
|
author,
|
||||||
content: JSON.parse(content)
|
thumbnailUrl,
|
||||||
} as IPublishNotebookRequest)
|
content: JSON.parse(content),
|
||||||
|
addLinkToNotebookViewer: isLinkInjectionEnabled
|
||||||
|
} as IPublishNotebookRequest)
|
||||||
|
: JSON.stringify({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
author,
|
||||||
|
thumbnailUrl,
|
||||||
|
content: JSON.parse(content)
|
||||||
|
} as IPublishNotebookRequest)
|
||||||
});
|
});
|
||||||
|
|
||||||
let data: IGalleryItem;
|
let data: IGalleryItem;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP status ${response.status} thrown. ${(await response.json()).Message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -341,11 +419,11 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getNotebooksUrl(): string {
|
private getNotebooksUrl(): string {
|
||||||
return `${config.JUNO_ENDPOINT}/api/notebooks`;
|
return `${configContext.JUNO_ENDPOINT}/api/notebooks`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNotebooksAccountUrl(): string {
|
private getNotebooksAccountUrl(): string {
|
||||||
return `${config.JUNO_ENDPOINT}/api/notebooks/${this.databaseAccount().name}`;
|
return `${configContext.JUNO_ENDPOINT}/api/notebooks/${this.databaseAccount().name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getHeaders(): HeadersInit {
|
private static getHeaders(): HeadersInit {
|
||||||
@@ -358,11 +436,11 @@ export class JunoClient {
|
|||||||
|
|
||||||
private static getGitHubClientParams(): URLSearchParams {
|
private static getGitHubClientParams(): URLSearchParams {
|
||||||
const githubParams = new URLSearchParams({
|
const githubParams = new URLSearchParams({
|
||||||
client_id: config.GITHUB_CLIENT_ID
|
client_id: configContext.GITHUB_CLIENT_ID
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.GITHUB_CLIENT_SECRET) {
|
if (configContext.GITHUB_CLIENT_SECRET) {
|
||||||
githubParams.append("client_secret", config.GITHUB_CLIENT_SECRET);
|
githubParams.append("client_secret", configContext.GITHUB_CLIENT_SECRET);
|
||||||
}
|
}
|
||||||
|
|
||||||
return githubParams;
|
return githubParams;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ import { AuthType } from "./AuthType";
|
|||||||
|
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
import { applyExplorerBindings } from "./applyExplorerBindings";
|
import { applyExplorerBindings } from "./applyExplorerBindings";
|
||||||
import { initializeConfiguration, Platform } from "./Config";
|
import { initializeConfiguration, Platform } from "./ConfigContext";
|
||||||
import Explorer from "./Explorer/Explorer";
|
import Explorer from "./Explorer/Explorer";
|
||||||
|
|
||||||
initializeIcons(/* optional base url */);
|
initializeIcons(/* optional base url */);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "bootstrap/dist/css/bootstrap.css";
|
|||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import { initializeConfiguration } from "../Config";
|
import { initializeConfiguration, configContext } from "../ConfigContext";
|
||||||
import {
|
import {
|
||||||
NotebookViewerComponent,
|
NotebookViewerComponent,
|
||||||
NotebookViewerComponentProps
|
NotebookViewerComponentProps
|
||||||
@@ -17,28 +17,41 @@ const onInit = async () => {
|
|||||||
await initializeConfiguration();
|
await initializeConfiguration();
|
||||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||||
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
|
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
|
||||||
const backNavigationText = galleryViewerProps.selectedTab && GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
|
let backNavigationText: string;
|
||||||
|
let onBackClick: () => void;
|
||||||
|
if (galleryViewerProps.selectedTab !== undefined) {
|
||||||
|
backNavigationText = GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
|
||||||
|
onBackClick = () => (window.location.href = `${configContext.hostedExplorerURL}gallery.html`);
|
||||||
|
}
|
||||||
const hideInputs = notebookViewerProps.hideInputs;
|
const hideInputs = notebookViewerProps.hideInputs;
|
||||||
|
|
||||||
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
|
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
|
||||||
render(notebookUrl, backNavigationText, hideInputs);
|
|
||||||
|
|
||||||
const galleryItemId = notebookViewerProps.galleryItemId;
|
const galleryItemId = notebookViewerProps.galleryItemId;
|
||||||
|
let galleryItem: IGalleryItem;
|
||||||
|
|
||||||
if (galleryItemId) {
|
if (galleryItemId) {
|
||||||
const junoClient = new JunoClient();
|
const junoClient = new JunoClient();
|
||||||
const notebook = await junoClient.getNotebook(galleryItemId);
|
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
|
||||||
render(notebookUrl, backNavigationText, hideInputs, notebook.data);
|
galleryItem = galleryItemJunoResponse.data;
|
||||||
}
|
}
|
||||||
|
render(notebookUrl, backNavigationText, hideInputs, galleryItem, onBackClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = (notebookUrl: string, backNavigationText: string, hideInputs: boolean, galleryItem?: IGalleryItem) => {
|
const render = (
|
||||||
|
notebookUrl: string,
|
||||||
|
backNavigationText: string,
|
||||||
|
hideInputs: boolean,
|
||||||
|
galleryItem?: IGalleryItem,
|
||||||
|
onBackClick?: () => void
|
||||||
|
) => {
|
||||||
const props: NotebookViewerComponentProps = {
|
const props: NotebookViewerComponentProps = {
|
||||||
junoClient: galleryItem ? new JunoClient() : undefined,
|
junoClient: galleryItem ? new JunoClient() : undefined,
|
||||||
notebookUrl,
|
notebookUrl,
|
||||||
galleryItem,
|
galleryItem,
|
||||||
backNavigationText,
|
backNavigationText,
|
||||||
hideInputs,
|
hideInputs,
|
||||||
onBackClick: undefined,
|
onBackClick: onBackClick,
|
||||||
onTagClick: undefined
|
onTagClick: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import AuthHeadersUtil from "./Authorization";
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels";
|
import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels";
|
||||||
import { config } from "../../Config";
|
import { configContext } from "../../ConfigContext";
|
||||||
|
|
||||||
// TODO: 421864 - add a fetch wrapper
|
// TODO: 421864 - add a fetch wrapper
|
||||||
export abstract class ArmResourceUtils {
|
export abstract class ArmResourceUtils {
|
||||||
private static readonly _armEndpoint: string = config.ARM_ENDPOINT;
|
private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT;
|
||||||
private static readonly _armApiVersion: string = config.ARM_API_VERSION;
|
private static readonly _armApiVersion: string = configContext.ARM_API_VERSION;
|
||||||
private static readonly _armAuthArea: string = config.ARM_AUTH_AREA;
|
private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA;
|
||||||
|
|
||||||
// TODO: 422867 - return continuation token instead of read through
|
// TODO: 422867 - return continuation token instead of read through
|
||||||
public static async listTenants(): Promise<Array<Tenant>> {
|
public static async listTenants(): Promise<Array<Tenant>> {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user