mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-25 20:54:18 +00:00
Compare commits
16 Commits
users/srna
...
remove-exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f738723f5a | ||
|
|
c0ce637eec | ||
|
|
b61a235bf6 | ||
|
|
0fa97c2ce9 | ||
|
|
fb71fb4e82 | ||
|
|
455722c316 | ||
|
|
5886db81e9 | ||
|
|
7a3e54d43e | ||
|
|
3051961093 | ||
|
|
abce15a6b2 | ||
|
|
a5b824ebb5 | ||
|
|
e28765d740 | ||
|
|
95f1efc03f | ||
|
|
455a6ac81b | ||
|
|
08ee86ecf1 | ||
|
|
0011007d5f |
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"
|
||||||
},
|
},
|
||||||
|
|||||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -10132,9 +10132,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",
|
||||||
@@ -21534,9 +21534,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 +24630,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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
TriggerDefinition
|
TriggerDefinition
|
||||||
} 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 +25,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 +53,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 +65,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 +80,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 +93,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 +107,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 +118,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 +133,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 +151,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 +174,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)
|
||||||
@@ -210,7 +209,7 @@ export function updateCollection(
|
|||||||
options: any = {}
|
options: any = {}
|
||||||
): Q.Promise<DataModels.Collection> {
|
): Q.Promise<DataModels.Collection> {
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
.container(collectionId)
|
.container(collectionId)
|
||||||
.replace(newCollection as ContainerDefinition, options)
|
.replace(newCollection as ContainerDefinition, options)
|
||||||
@@ -228,7 +227,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,7 +242,7 @@ 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)
|
.replace(newOffer, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -258,7 +257,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 +272,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 +287,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 +298,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 +312,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 +326,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 +340,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 +352,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 +368,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 +382,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 +396,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 +410,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 +418,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 +428,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)
|
||||||
@@ -476,7 +455,7 @@ export function readCollectionQuotaInfo(
|
|||||||
|
|
||||||
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
|
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
|
||||||
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,7 +465,7 @@ 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)
|
||||||
@@ -501,33 +480,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 +528,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(
|
||||||
@@ -612,7 +571,7 @@ export function createDatabase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +579,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,36 +592,13 @@ 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> {
|
function _createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise<DataModels.Database> {
|
||||||
const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request;
|
const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request;
|
||||||
const createBody: DatabaseRequest = { id: databaseId };
|
const createBody: DatabaseRequest = { id: databaseId };
|
||||||
@@ -685,7 +621,7 @@ function _createDatabase(request: DataModels.CreateDatabaseRequest, options: any
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Q(
|
return Q(
|
||||||
CosmosClient.client()
|
client()
|
||||||
.databases.create(createBody, databaseOptions)
|
.databases.create(createBody, databaseOptions)
|
||||||
.then((response: DatabaseResponse) => {
|
.then((response: DatabaseResponse) => {
|
||||||
return refreshCachedResources(databaseOptions).then(() => response.resource);
|
return refreshCachedResources(databaseOptions).then(() => response.resource);
|
||||||
|
|||||||
@@ -383,40 +383,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 +806,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 +893,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 = {}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import "jquery";
|
|
||||||
import * as Q from "q";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
|
||||||
|
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
|
||||||
import { CosmosClient } from "./CosmosClient";
|
|
||||||
|
|
||||||
export class NotificationsClientBase {
|
|
||||||
private _extensionEndpoint: string;
|
|
||||||
private _notificationsApiSuffix: string;
|
|
||||||
|
|
||||||
protected constructor(notificationsApiSuffix: string) {
|
|
||||||
this._notificationsApiSuffix = notificationsApiSuffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
|
|
||||||
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
|
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
|
||||||
const subscriptionId: string = CosmosClient.subscriptionId();
|
|
||||||
const resourceGroup: string = CosmosClient.resourceGroup();
|
|
||||||
const url: string = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
|
||||||
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
|
||||||
const headers: any = {};
|
|
||||||
headers[authorizationHeader.header] = authorizationHeader.token;
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: url,
|
|
||||||
type: "GET",
|
|
||||||
headers: headers,
|
|
||||||
cache: false
|
|
||||||
}).then(
|
|
||||||
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
|
|
||||||
deferred.resolve(notifications);
|
|
||||||
},
|
|
||||||
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
|
|
||||||
deferred.reject(xhr.responseText);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setExtensionEndpoint(extensionEndpoint: string): void {
|
|
||||||
this._extensionEndpoint = extensionEndpoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
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";
|
||||||
|
|
||||||
@@ -11,14 +16,9 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.authType === AuthType.AAD) {
|
if (window.authType === AuthType.AAD) {
|
||||||
await deleteSqlDatabase(
|
await deleteDatabaseWithARM(databaseId);
|
||||||
CosmosClient.subscriptionId(),
|
|
||||||
CosmosClient.resourceGroup(),
|
|
||||||
CosmosClient.databaseAccount().name,
|
|
||||||
databaseId
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await CosmosClient.client()
|
await client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
@@ -32,3 +32,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();
|
||||||
|
});
|
||||||
|
});
|
||||||
66
src/Common/dataAccess/readCollections.ts
Normal file
66
src/Common/dataAccess/readCollections.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
61
src/Common/dataAccess/readDatabases.ts
Normal file
61
src/Common/dataAccess/readDatabases.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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,22 +46,35 @@ 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);
|
||||||
@@ -70,12 +83,13 @@ export async function initializeConfiguration(): Promise<Config> {
|
|||||||
// Allow override of any config value with URL query parameters
|
// Allow override of any config value with URL query parameters
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
params.forEach((value, key) => {
|
params.forEach((value, key) => {
|
||||||
(config as any)[key] = value;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(configContext as any)[key] = value;
|
||||||
});
|
});
|
||||||
} 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 };
|
||||||
@@ -312,17 +312,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;
|
||||||
|
|||||||
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 } 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";
|
||||||
@@ -81,10 +82,10 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
|
|||||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||||
import StoredProcedure from "./Tree/StoredProcedure";
|
import StoredProcedure from "./Tree/StoredProcedure";
|
||||||
import Trigger from "./Tree/Trigger";
|
import Trigger from "./Tree/Trigger";
|
||||||
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
|
||||||
@@ -96,7 +97,6 @@ enum ShareAccessToggleState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ExplorerOptions {
|
interface ExplorerOptions {
|
||||||
notificationsClient: NotificationsClientBase;
|
|
||||||
isEmulator: boolean;
|
isEmulator: boolean;
|
||||||
}
|
}
|
||||||
interface AdHocAccessData {
|
interface AdHocAccessData {
|
||||||
@@ -131,7 +131,6 @@ export default class Explorer {
|
|||||||
public isPreferredApiTable: ko.Computed<boolean>;
|
public isPreferredApiTable: ko.Computed<boolean>;
|
||||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||||
public isServerlessEnabled: ko.Computed<boolean>;
|
public isServerlessEnabled: ko.Computed<boolean>;
|
||||||
public isEmulator: boolean;
|
|
||||||
public isAccountReady: ko.Observable<boolean>;
|
public isAccountReady: ko.Observable<boolean>;
|
||||||
public canSaveQueries: ko.Computed<boolean>;
|
public canSaveQueries: ko.Computed<boolean>;
|
||||||
public features: ko.Observable<any>;
|
public features: ko.Observable<any>;
|
||||||
@@ -139,7 +138,6 @@ export default class Explorer {
|
|||||||
public extensionEndpoint: ko.Observable<string>;
|
public extensionEndpoint: ko.Observable<string>;
|
||||||
public armEndpoint: ko.Observable<string>;
|
public armEndpoint: ko.Observable<string>;
|
||||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||||
public notificationsClient: NotificationsClientBase;
|
|
||||||
public queriesClient: QueriesClient;
|
public queriesClient: QueriesClient;
|
||||||
public tableDataClient: TableDataClient;
|
public tableDataClient: TableDataClient;
|
||||||
public splitter: Splitter;
|
public splitter: Splitter;
|
||||||
@@ -203,11 +201,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>;
|
||||||
@@ -263,7 +265,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||||
|
|
||||||
constructor(options: ExplorerOptions) {
|
constructor() {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
@@ -369,8 +371,6 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
||||||
this.notificationsClient = options.notificationsClient;
|
|
||||||
this.isEmulator = options.isEmulator;
|
|
||||||
|
|
||||||
this.features = ko.observable();
|
this.features = ko.observable();
|
||||||
this.serverId = ko.observable<string>();
|
this.serverId = ko.observable<string>();
|
||||||
@@ -408,8 +408,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 +478,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(() => {
|
||||||
@@ -1403,7 +1416,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(
|
||||||
@@ -1603,7 +1616,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 +1719,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 +1732,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 +1744,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 +1826,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,8 +1932,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.databaseAccount(databaseAccount);
|
this.databaseAccount(databaseAccount);
|
||||||
this.subscriptionType(inputs.subscriptionType);
|
this.subscriptionType(inputs.subscriptionType);
|
||||||
this.quotaId(inputs.quotaId);
|
this.quotaId(inputs.quotaId);
|
||||||
@@ -1930,11 +1947,12 @@ export default class Explorer {
|
|||||||
|
|
||||||
this._importExplorerConfigComplete = true;
|
this._importExplorerConfigComplete = true;
|
||||||
|
|
||||||
CosmosClient.authorizationToken(authorizationToken);
|
updateUserContext({
|
||||||
CosmosClient.masterKey(masterKey);
|
authorizationToken,
|
||||||
CosmosClient.databaseAccount(databaseAccount);
|
masterKey,
|
||||||
CosmosClient.subscriptionId(inputs.subscriptionId);
|
databaseAccount
|
||||||
CosmosClient.resourceGroup(inputs.resourceGroup);
|
});
|
||||||
|
updateUserContext({ resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId });
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadDatabaseAccount,
|
Action.LoadDatabaseAccount,
|
||||||
{
|
{
|
||||||
@@ -2179,7 +2197,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 +2309,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 +2364,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 +2487,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 +2684,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 +2713,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 +2742,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 +2933,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 +2972,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 +3019,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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ export class CommandBarComponentButtonFactory {
|
|||||||
buttons.push(fullScreenButton);
|
buttons.push(fullScreenButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
|
if (configContext.platform !== Platform.Emulator) {
|
||||||
const label = "Feedback";
|
const label = "Feedback";
|
||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
@@ -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)";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe("Add Collection Pane", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -329,7 +329,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
if (
|
if (
|
||||||
!this.container.isEmulator &&
|
configContext.platform !== Platform.Emulator &&
|
||||||
!this.container.isTryCosmosDBSubscription() &&
|
!this.container.isTryCosmosDBSubscription() &&
|
||||||
this.container.getPlatformType() !== PlatformType.Portal
|
this.container.getPlatformType() !== PlatformType.Portal
|
||||||
) {
|
) {
|
||||||
@@ -341,7 +341,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.costsVisible = ko.pureComputed(() => {
|
this.costsVisible = ko.pureComputed(() => {
|
||||||
return !this.container.isEmulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.maxCollectionsReached = ko.computed<boolean>(() => {
|
this.maxCollectionsReached = 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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,10 +40,7 @@ describe("Add Database Pane", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({
|
explorer = new Explorer();
|
||||||
notificationsClient: null,
|
|
||||||
isEmulator: false
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if subscription type is Benefits", () => {
|
it("should be true if subscription type is Benefits", () => {
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ 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 { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
|
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
|
|
||||||
export default class AddDatabasePane extends ContextualPaneBase {
|
export default class AddDatabasePane extends ContextualPaneBase {
|
||||||
public defaultExperience: ko.Computed<string>;
|
public defaultExperience: ko.Computed<string>;
|
||||||
@@ -179,7 +180,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
if (
|
if (
|
||||||
!this.container.isEmulator &&
|
configContext.platform !== Platform.Emulator &&
|
||||||
!this.container.isTryCosmosDBSubscription() &&
|
!this.container.isTryCosmosDBSubscription() &&
|
||||||
this.container.getPlatformType() !== PlatformType.Portal
|
this.container.getPlatformType() !== PlatformType.Portal
|
||||||
) {
|
) {
|
||||||
@@ -202,7 +203,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.costsVisible = ko.pureComputed(() => {
|
this.costsVisible = ko.pureComputed(() => {
|
||||||
return !this.container.isEmulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
|
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
|
||||||
@@ -308,8 +309,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
db: addDatabasePaneStartMessage.database.id,
|
db: addDatabasePaneStartMessage.database.id,
|
||||||
st: addDatabasePaneStartMessage.database.shared,
|
st: addDatabasePaneStartMessage.database.shared,
|
||||||
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
|
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
|
||||||
sid: CosmosClient.subscriptionId(),
|
sid: userContext.subscriptionId,
|
||||||
rg: CosmosClient.resourceGroup(),
|
rg: userContext.resourceGroup,
|
||||||
dba: addDatabasePaneStartMessage.databaseAccountName
|
dba: addDatabasePaneStartMessage.databaseAccountName
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
|||||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
import { HashMap } from "../../Common/HashMap";
|
import { HashMap } from "../../Common/HashMap";
|
||||||
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
|
|
||||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||||
public createTableQuery: ko.Observable<string>;
|
public createTableQuery: ko.Observable<string>;
|
||||||
@@ -231,11 +232,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.costsVisible = ko.pureComputed(() => {
|
this.costsVisible = ko.pureComputed(() => {
|
||||||
return !this.container.isEmulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
if (!this.container.isEmulator && !this.container.isTryCosmosDBSubscription()) {
|
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
|
||||||
const offerThroughput: number = this.throughput();
|
const offerThroughput: number = this.throughput();
|
||||||
return offerThroughput <= 100000;
|
return offerThroughput <= 100000;
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if 1 database and 1 collection", () => {
|
it("should be true if 1 database and 1 collection", () => {
|
||||||
@@ -56,7 +56,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
|
|
||||||
describe("shouldRecordFeedback()", () => {
|
describe("shouldRecordFeedback()", () => {
|
||||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||||
let fakeExplorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
let fakeExplorer = new Explorer();
|
||||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe("Delete Database Confirmation Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true if only 1 database", () => {
|
it("should be true if only 1 database", () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
isSubmitButtonVisible?: 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.isSubmitButtonVisible ? "visible" : "hidden" }}
|
||||||
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(),
|
||||||
|
isSubmitButtonVisible: 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>
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe("Settings Pane", () => {
|
|||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true for SQL API", () => {
|
it("should be true for SQL API", () => {
|
||||||
|
|||||||
@@ -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,19 +33,30 @@ 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",
|
||||||
isExecuting: this.isExecuting,
|
isExecuting: this.isExecuting,
|
||||||
|
isSubmitButtonVisible: true,
|
||||||
title: "Upload Items",
|
title: "Upload Items",
|
||||||
submitButtonText: "Upload",
|
submitButtonText: "Upload",
|
||||||
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 +119,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 +150,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 [
|
||||||
"",
|
"",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Explorer from "../Explorer";
|
|||||||
jest.mock("../Explorer");
|
jest.mock("../Explorer");
|
||||||
|
|
||||||
const createExplorer = () => {
|
const createExplorer = () => {
|
||||||
const mock = new Explorer({} as any);
|
const mock = new Explorer();
|
||||||
mock.selectedNode = ko.observable();
|
mock.selectedNode = ko.observable();
|
||||||
mock.isNotebookEnabled = ko.observable(false);
|
mock.isNotebookEnabled = ko.observable(false);
|
||||||
mock.addCollectionText = ko.observable("add collection");
|
mock.addCollectionText = ko.observable("add collection");
|
||||||
|
|||||||
@@ -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,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 { 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";
|
||||||
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
|
|
||||||
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.
|
||||||
@@ -196,7 +197,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.costsVisible = ko.computed(() => {
|
this.costsVisible = ko.computed(() => {
|
||||||
return !this.container.isEmulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(
|
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(
|
||||||
@@ -207,7 +208,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
);
|
);
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
if (
|
if (
|
||||||
!!this.container.isEmulator ||
|
configContext.platform === Platform.Emulator ||
|
||||||
this.container.getPlatformType() === PlatformType.Hosted ||
|
this.container.getPlatformType() === PlatformType.Hosted ||
|
||||||
this.canThroughputExceedMaximumValue()
|
this.canThroughputExceedMaximumValue()
|
||||||
) {
|
) {
|
||||||
@@ -520,16 +521,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 +553,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: {
|
||||||
|
|||||||
@@ -27,15 +27,9 @@ describe("Documents tab", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("showPartitionKey", () => {
|
describe("showPartitionKey", () => {
|
||||||
const explorer = new Explorer({
|
const explorer = new Explorer();
|
||||||
notificationsClient: null,
|
|
||||||
isEmulator: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const mongoExplorer = new Explorer({
|
const mongoExplorer = new Explorer();
|
||||||
notificationsClient: null,
|
|
||||||
isEmulator: false
|
|
||||||
});
|
|
||||||
mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
|
mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
|
||||||
|
|
||||||
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
|
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -164,9 +166,17 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ariaLabel: saveLabel
|
ariaLabel: saveLabel
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
iconName: "Copy",
|
||||||
|
onCommandClick: () => this.copyNotebook(),
|
||||||
|
commandButtonLabel: copyToLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: copyToLabel
|
||||||
|
},
|
||||||
{
|
{
|
||||||
iconName: "PublishContent",
|
iconName: "PublishContent",
|
||||||
onCommandClick: () => this.publishToGallery(),
|
onCommandClick: async () => await this.publishToGallery(),
|
||||||
commandButtonLabel: publishLabel,
|
commandButtonLabel: publishLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@@ -423,7 +433,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 +466,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,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("Query Tab", () => {
|
|||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be true for accounts using SQL API", () => {
|
it("should be true for accounts using SQL API", () => {
|
||||||
@@ -69,7 +69,7 @@ describe("Query Tab", () => {
|
|||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be visible when using a supported API", () => {
|
it("should be visible when using a supported API", () => {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe("Settings tab", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ describe("Settings tab", () => {
|
|||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ describe("Settings tab", () => {
|
|||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
explorer = new Explorer();
|
||||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,10 +336,7 @@ describe("Settings tab", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
|
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
|
||||||
const explorer = new Explorer({
|
const explorer = new Explorer();
|
||||||
notificationsClient: null,
|
|
||||||
isEmulator: false
|
|
||||||
});
|
|
||||||
explorer.defaultExperience(defaultApi);
|
explorer.defaultExperience(defaultApi);
|
||||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
|
|
||||||
@@ -471,10 +468,7 @@ describe("Settings tab", () => {
|
|||||||
|
|
||||||
describe("AutoPilot", () => {
|
describe("AutoPilot", () => {
|
||||||
function getCollection(autoPilotTier: DataModels.AutopilotTier) {
|
function getCollection(autoPilotTier: DataModels.AutopilotTier) {
|
||||||
const explorer = new Explorer({
|
const explorer = new Explorer();
|
||||||
notificationsClient: null,
|
|
||||||
isEmulator: false
|
|
||||||
});
|
|
||||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||||
|
|
||||||
explorer.databaseAccount({
|
explorer.databaseAccount({
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ 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, updateCollection } from "../../Common/DocumentClientUtilityBase";
|
||||||
updateOfferThroughputBeyondLimit,
|
|
||||||
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";
|
||||||
|
import { config } from "process";
|
||||||
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
|
|
||||||
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 +346,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;
|
||||||
}
|
}
|
||||||
@@ -457,7 +456,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.rupmVisible = ko.computed(() => {
|
this.rupmVisible = ko.computed(() => {
|
||||||
if (this.container.isEmulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.container.isFeatureEnabled(Constants.Features.enableRupm)) {
|
if (this.container.isFeatureEnabled(Constants.Features.enableRupm)) {
|
||||||
@@ -487,7 +486,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.costsVisible = ko.computed(() => {
|
this.costsVisible = ko.computed(() => {
|
||||||
return !this.container.isEmulator;
|
return configContext.platform !== Platform.Emulator;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isTryCosmosDBSubscription = ko.computed<boolean>(() => {
|
this.isTryCosmosDBSubscription = ko.computed<boolean>(() => {
|
||||||
@@ -503,7 +502,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.canRequestSupport = ko.pureComputed(() => {
|
this.canRequestSupport = ko.pureComputed(() => {
|
||||||
if (this.container.isEmulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,7 +709,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs();
|
const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs();
|
||||||
const isEmulator = this.container.isEmulator;
|
const isEmulator = configContext.platform === Platform.Emulator;
|
||||||
if (isThroughputGreaterThanMaxRus && isEmulator) {
|
if (isThroughputGreaterThanMaxRus && isEmulator) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -881,7 +880,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
|
||||||
const throughputExceedsMaxValue: boolean = !this.container.isEmulator && this.throughput() > this.maxRUs();
|
const throughputExceedsMaxValue: boolean =
|
||||||
|
configContext.platform !== Platform.Emulator && this.throughput() > this.maxRUs();
|
||||||
|
|
||||||
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
|
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
|
||||||
const ttlOrIndexingPolicyFieldsDirty: boolean =
|
const ttlOrIndexingPolicyFieldsDirty: boolean =
|
||||||
@@ -1144,16 +1144,16 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
this.container != null
|
this.container != null
|
||||||
) {
|
) {
|
||||||
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.collection.databaseId,
|
databaseName: this.collection.databaseId,
|
||||||
collectionName: this.collection.id(),
|
collectionName: this.collection.id(),
|
||||||
throughput: newThroughput,
|
throughput: newThroughput,
|
||||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||||
};
|
};
|
||||||
const updateOfferBeyondLimitPromise: Q.Promise<void> = updateOfferThroughputBeyondLimit(requestPayload).then(
|
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
|
||||||
() => {
|
() => {
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||||
this.throughput(originalThroughputValue);
|
this.throughput(originalThroughputValue);
|
||||||
@@ -1185,7 +1185,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
promises.push(updateOfferBeyondLimitPromise);
|
promises.push(Q(updateOfferBeyondLimitPromise));
|
||||||
} else {
|
} else {
|
||||||
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
|
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
|
||||||
(updatedOffer: DataModels.Offer) => {
|
(updatedOffer: DataModels.Offer) => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe("Tabs manager tests", () => {
|
|||||||
let documentsTab: DocumentsTab;
|
let documentsTab: DocumentsTab;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
explorer = new Explorer({ notificationsClient: undefined, isEmulator: false });
|
explorer = new Explorer();
|
||||||
explorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
|
explorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
|
||||||
id: "test",
|
id: "test",
|
||||||
name: "test",
|
name: "test",
|
||||||
|
|||||||
@@ -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(),
|
||||||
@@ -559,7 +559,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
||||||
return tab.collection && tab.collection.rid === this.rid;
|
return tab.collection && tab.collection.rid === this.rid;
|
||||||
});
|
});
|
||||||
@@ -575,9 +574,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
tabTitle: tabTitle
|
tabTitle: tabTitle
|
||||||
});
|
});
|
||||||
|
|
||||||
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
|
Q.all([this.readSettings()]).then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
const pendingNotification: DataModels.Notification = data && data[0];
|
|
||||||
settingsTab = new SettingsTab({
|
settingsTab = new SettingsTab({
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||||
title: !this.offer() ? "Settings" : "Scale & Settings",
|
title: !this.offer() ? "Settings" : "Scale & Settings",
|
||||||
@@ -592,7 +590,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||||
});
|
});
|
||||||
this.container.tabsManager.activateNewTab(settingsTab);
|
this.container.tabsManager.activateNewTab(settingsTab);
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
@@ -616,16 +613,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
pendingNotificationsPromise.then(
|
this.container.tabsManager.activateTab(settingsTab);
|
||||||
(pendingNotification: DataModels.Notification) => {
|
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
this.container.tabsManager.activateTab(settingsTab);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
settingsTab.pendingNotification(undefined);
|
|
||||||
this.container.tabsManager.activateTab(settingsTab);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -804,14 +792,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 +1169,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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1283,48 +1271,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
|
||||||
if (!this.container) {
|
|
||||||
return Q.resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
|
||||||
this.container.notificationsClient.fetchNotifications().then(
|
|
||||||
(notifications: DataModels.Notification[]) => {
|
|
||||||
if (!notifications || notifications.length === 0) {
|
|
||||||
deferred.resolve(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
|
|
||||||
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
|
||||||
return (
|
|
||||||
notification.kind === "message" &&
|
|
||||||
notification.collectionName === this.id() &&
|
|
||||||
notification.description &&
|
|
||||||
throughputUpdateRegExp.test(notification.description)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
deferred.resolve(pendingNotification);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
Logger.logError(
|
|
||||||
JSON.stringify({
|
|
||||||
error: JSON.stringify(error),
|
|
||||||
accountName: this.container && this.container.databaseAccount(),
|
|
||||||
databaseName: this.databaseId,
|
|
||||||
collectionName: this.id()
|
|
||||||
}),
|
|
||||||
"Settings tree node"
|
|
||||||
);
|
|
||||||
deferred.resolve(undefined);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
||||||
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
|
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
|
||||||
const numFiles: number = uploadDetailsRecords.length;
|
const numFiles: number = uploadDetailsRecords.length;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -51,7 +52,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
|
|
||||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(
|
const matchingTabs = this.container.tabsManager.getTabs(
|
||||||
ViewModels.CollectionTabKind.DatabaseSettings,
|
ViewModels.CollectionTabKind.DatabaseSettings,
|
||||||
tab => tab.rid === this.rid
|
tab => tab.rid === this.rid
|
||||||
@@ -65,9 +65,8 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: "Scale"
|
tabTitle: "Scale"
|
||||||
});
|
});
|
||||||
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
|
Q.all([this.readSettings()]).then(
|
||||||
(data: any) => {
|
() => {
|
||||||
const pendingNotification: DataModels.Notification = data && data[0];
|
|
||||||
settingsTab = new DatabaseSettingsTab({
|
settingsTab = new DatabaseSettingsTab({
|
||||||
tabKind: ViewModels.CollectionTabKind.DatabaseSettings,
|
tabKind: ViewModels.CollectionTabKind.DatabaseSettings,
|
||||||
title: "Scale",
|
title: "Scale",
|
||||||
@@ -82,7 +81,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
this.container.tabsManager.activateNewTab(settingsTab);
|
this.container.tabsManager.activateNewTab(settingsTab);
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
@@ -107,16 +105,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
pendingNotificationsPromise.then(
|
this.container.tabsManager.activateTab(settingsTab);
|
||||||
(pendingNotification: DataModels.Notification) => {
|
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
this.container.tabsManager.activateTab(settingsTab);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
settingsTab.pendingNotification(undefined);
|
|
||||||
this.container.tabsManager.activateTab(settingsTab);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,7 +248,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);
|
||||||
@@ -292,49 +281,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
|
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
|
||||||
if (!this.container) {
|
|
||||||
return Q.resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
|
||||||
this.container.notificationsClient.fetchNotifications().then(
|
|
||||||
(notifications: DataModels.Notification[]) => {
|
|
||||||
if (!notifications || notifications.length === 0) {
|
|
||||||
deferred.resolve(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
|
|
||||||
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
|
||||||
return (
|
|
||||||
notification.kind === "message" &&
|
|
||||||
!notification.collectionName &&
|
|
||||||
notification.databaseName === this.id() &&
|
|
||||||
notification.description &&
|
|
||||||
throughputUpdateRegExp.test(notification.description)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
deferred.resolve(pendingNotification);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
Logger.logError(
|
|
||||||
JSON.stringify({
|
|
||||||
error: JSON.stringify(error),
|
|
||||||
accountName: this.container && this.container.databaseAccount(),
|
|
||||||
databaseName: this.id(),
|
|
||||||
collectionName: this.id()
|
|
||||||
}),
|
|
||||||
"Settings tree node"
|
|
||||||
);
|
|
||||||
deferred.resolve(undefined);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDeltaCollections(
|
private getDeltaCollections(
|
||||||
updatedCollectionsList: DataModels.Collection[]
|
updatedCollectionsList: DataModels.Collection[]
|
||||||
): { toAdd: DataModels.Collection[]; toDelete: Collection[] } {
|
): { toAdd: DataModels.Collection[]; toDelete: Collection[] } {
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -563,6 +567,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Copy to ...",
|
||||||
|
iconSrc: CopyIcon,
|
||||||
|
onClick: () => this.copyNotebook(item)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Download",
|
label: "Download",
|
||||||
iconSrc: NotebookIcon,
|
iconSrc: NotebookIcon,
|
||||||
@@ -574,6 +583,13 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,14 +2,9 @@ import { AccountKind, TagNames, DefaultAccountExperience } from "../../Common/Co
|
|||||||
|
|
||||||
import Explorer from "../../Explorer/Explorer";
|
import Explorer from "../../Explorer/Explorer";
|
||||||
|
|
||||||
import { NotificationsClient } from "./NotificationsClient";
|
|
||||||
|
|
||||||
export default class EmulatorExplorerFactory {
|
export default class EmulatorExplorerFactory {
|
||||||
public static createExplorer(): Explorer {
|
public static createExplorer(): Explorer {
|
||||||
const explorer: Explorer = new Explorer({
|
const explorer: Explorer = new Explorer();
|
||||||
notificationsClient: new NotificationsClient(),
|
|
||||||
isEmulator: true
|
|
||||||
});
|
|
||||||
explorer.databaseAccount({
|
explorer.databaseAccount({
|
||||||
name: "",
|
name: "",
|
||||||
id: "",
|
id: "",
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
import EmulatorExplorerFactory from "./ExplorerFactory";
|
|
||||||
import Explorer from "../../Explorer/Explorer";
|
import Explorer from "../../Explorer/Explorer";
|
||||||
|
import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants";
|
||||||
|
|
||||||
export function initializeExplorer(): Explorer {
|
export function initializeExplorer(): Explorer {
|
||||||
return EmulatorExplorerFactory.createExplorer();
|
const explorer: Explorer = new Explorer();
|
||||||
|
explorer.databaseAccount({
|
||||||
|
name: "",
|
||||||
|
id: "",
|
||||||
|
location: "",
|
||||||
|
type: "",
|
||||||
|
kind: AccountKind.DocumentDB,
|
||||||
|
tags: {
|
||||||
|
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
documentEndpoint: "",
|
||||||
|
tableEndpoint: "",
|
||||||
|
gremlinEndpoint: "",
|
||||||
|
cassandraEndpoint: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
explorer.isAccountReady(true);
|
||||||
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import Q from "q";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { NotificationsClientBase } from "../../Common/NotificationsClientBase";
|
|
||||||
|
|
||||||
export class NotificationsClient extends NotificationsClientBase {
|
|
||||||
private static readonly _notificationsApiSuffix: string = "/api/notifications";
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
super(NotificationsClient._notificationsApiSuffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
|
|
||||||
// no notifications for the emulator
|
|
||||||
return Q([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>> {
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import "expose-loader?AuthenticationContext!../../../externals/adal";
|
|||||||
import Q from "q";
|
import Q from "q";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { config } from "../../Config";
|
import { configContext } from "../../ConfigContext";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class AuthHeadersUtil {
|
export default class AuthHeadersUtil {
|
||||||
// TODO: Figure out a way to determine the extension endpoint and serverId at runtime
|
// TODO: Figure out a way to determine the extension endpoint and serverId at runtime
|
||||||
@@ -18,12 +17,12 @@ export default class AuthHeadersUtil {
|
|||||||
public static serverId: string = Constants.ServerIds.productionPortal;
|
public static serverId: string = Constants.ServerIds.productionPortal;
|
||||||
|
|
||||||
private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d";
|
private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d";
|
||||||
private static readonly _aadEndpoint: string = config.AAD_ENDPOINT;
|
private static readonly _aadEndpoint: string = configContext.AAD_ENDPOINT;
|
||||||
private static readonly _armEndpoint: string = config.ARM_ENDPOINT;
|
private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT;
|
||||||
private static readonly _arcadiaEndpoint: string = config.ARCADIA_ENDPOINT;
|
private static readonly _arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT;
|
||||||
private static readonly _armAuthArea: string = config.ARM_AUTH_AREA;
|
private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA;
|
||||||
private static readonly _graphEndpoint: string = config.GRAPH_ENDPOINT;
|
private static readonly _graphEndpoint: string = configContext.GRAPH_ENDPOINT;
|
||||||
private static readonly _graphApiVersion: string = config.GRAPH_API_VERSION;
|
private static readonly _graphApiVersion: string = configContext.GRAPH_API_VERSION;
|
||||||
|
|
||||||
private static _authContext: AuthenticationContext = new AuthenticationContext({
|
private static _authContext: AuthenticationContext = new AuthenticationContext({
|
||||||
instance: AuthHeadersUtil._aadEndpoint,
|
instance: AuthHeadersUtil._aadEndpoint,
|
||||||
@@ -91,7 +90,7 @@ export default class AuthHeadersUtil {
|
|||||||
AuthHeadersUtil.extensionEndpoint
|
AuthHeadersUtil.extensionEndpoint
|
||||||
}/api/tokens/generateToken${AuthHeadersUtil._generateResourceUrl()}`;
|
}/api/tokens/generateToken${AuthHeadersUtil._generateResourceUrl()}`;
|
||||||
const explorer = window.dataExplorer;
|
const explorer = window.dataExplorer;
|
||||||
const headers: any = { authorization: CosmosClient.authorizationToken() };
|
const headers: any = { authorization: userContext.authorizationToken };
|
||||||
headers[Constants.HttpHeaders.getReadOnlyKey] = !explorer.hasWriteAccess();
|
headers[Constants.HttpHeaders.getReadOnlyKey] = !explorer.hasWriteAccess();
|
||||||
|
|
||||||
return AuthHeadersUtil._initiateGenerateTokenRequest({
|
return AuthHeadersUtil._initiateGenerateTokenRequest({
|
||||||
@@ -272,9 +271,9 @@ export default class AuthHeadersUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static _generateResourceUrl(): string {
|
private static _generateResourceUrl(): string {
|
||||||
const databaseAccount = CosmosClient.databaseAccount();
|
const databaseAccount = userContext.databaseAccount;
|
||||||
const subscriptionId: string = CosmosClient.subscriptionId();
|
const subscriptionId: string = userContext.subscriptionId;
|
||||||
const resourceGroup: string = CosmosClient.resourceGroup();
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount);
|
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount);
|
||||||
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience);
|
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience);
|
||||||
const accountEndpoint = (databaseAccount && databaseAccount.properties.documentEndpoint) || "";
|
const accountEndpoint = (databaseAccount && databaseAccount.properties.documentEndpoint) || "";
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import Explorer from "../../Explorer/Explorer";
|
import Explorer from "../../Explorer/Explorer";
|
||||||
import { NotificationsClient } from "./NotificationsClient";
|
|
||||||
|
|
||||||
export default class HostedExplorerFactory {
|
export default class HostedExplorerFactory {
|
||||||
public createExplorer(): Explorer {
|
public createExplorer(): Explorer {
|
||||||
const explorer = new Explorer({
|
return new Explorer();
|
||||||
notificationsClient: new NotificationsClient(),
|
|
||||||
isEmulator: false
|
|
||||||
});
|
|
||||||
|
|
||||||
return explorer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static reInitializeDocumentClientUtilityForExplorer(explorer: Explorer): void {
|
public static reInitializeDocumentClientUtilityForExplorer(explorer: Explorer): void {
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ export class ConnectionStringParser {
|
|||||||
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
|
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
|
||||||
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
||||||
accessInput.apiKind = DataModels.ApiKind.MongoDBCompute;
|
accessInput.apiKind = DataModels.ApiKind.MongoDBCompute;
|
||||||
} else if (RegExp(Constants.EndpointsRegex.cassandra).test(connectionStringPart)) {
|
} else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) {
|
||||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.cassandra)[1];
|
Constants.EndpointsRegex.cassandra.forEach(regex => {
|
||||||
accessInput.apiKind = DataModels.ApiKind.Cassandra;
|
if (RegExp(regex).test(connectionStringPart)) {
|
||||||
|
accessInput.accountName = connectionStringPart.match(regex)[1];
|
||||||
|
accessInput.apiKind = DataModels.ApiKind.Cassandra;
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
|
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
|
||||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
|
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
|
||||||
accessInput.apiKind = DataModels.ApiKind.Table;
|
accessInput.apiKind = DataModels.ApiKind.Table;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { CollectionCreation } from "../../Shared/Constants";
|
import { CollectionCreation } from "../../Shared/Constants";
|
||||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||||
import { CosmosClient } from "../../Common/CosmosClient";
|
|
||||||
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
import { DataExplorerInputsFrame } from "../../Contracts/ViewModels";
|
||||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||||
import { HostedUtils } from "./HostedUtils";
|
import { HostedUtils } from "./HostedUtils";
|
||||||
@@ -24,6 +23,7 @@ import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
|||||||
import { SubscriptionUtilMappings } from "../../Shared/Constants";
|
import { SubscriptionUtilMappings } from "../../Shared/Constants";
|
||||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||||
import Explorer from "../../Explorer/Explorer";
|
import Explorer from "../../Explorer/Explorer";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class Main {
|
export default class Main {
|
||||||
private static _databaseAccountId: string;
|
private static _databaseAccountId: string;
|
||||||
@@ -84,7 +84,9 @@ export default class Main {
|
|||||||
displayText: "Loading..."
|
displayText: "Loading..."
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
CosmosClient.accessToken(Main._encryptedToken);
|
updateUserContext({
|
||||||
|
accessToken: Main._encryptedToken
|
||||||
|
});
|
||||||
Main._getAccessInputMetadata(Main._encryptedToken).then(
|
Main._getAccessInputMetadata(Main._encryptedToken).then(
|
||||||
() => {
|
() => {
|
||||||
const expiryTimestamp: number =
|
const expiryTimestamp: number =
|
||||||
@@ -203,7 +205,9 @@ export default class Main {
|
|||||||
Main._encryptedToken = encryptedToken.readWrite;
|
Main._encryptedToken = encryptedToken.readWrite;
|
||||||
window.authType = AuthType.EncryptedToken;
|
window.authType = AuthType.EncryptedToken;
|
||||||
|
|
||||||
CosmosClient.accessToken(Main._encryptedToken);
|
updateUserContext({
|
||||||
|
accessToken: Main._encryptedToken
|
||||||
|
});
|
||||||
Main._getAccessInputMetadata(Main._encryptedToken).then(
|
Main._getAccessInputMetadata(Main._encryptedToken).then(
|
||||||
() => {
|
() => {
|
||||||
if (explorer.isConnectExplorerVisible()) {
|
if (explorer.isConnectExplorerVisible()) {
|
||||||
@@ -472,8 +476,10 @@ export default class Main {
|
|||||||
console.error("Invalid connection string input");
|
console.error("Invalid connection string input");
|
||||||
Q.reject("Invalid connection string input");
|
Q.reject("Invalid connection string input");
|
||||||
}
|
}
|
||||||
CosmosClient.resourceToken(properties.resourceToken);
|
updateUserContext({
|
||||||
CosmosClient.endpoint(properties.accountEndpoint);
|
resourceToken: properties.resourceToken,
|
||||||
|
endpoint: properties.accountEndpoint
|
||||||
|
});
|
||||||
explorer.resourceTokenDatabaseId(properties.databaseId);
|
explorer.resourceTokenDatabaseId(properties.databaseId);
|
||||||
explorer.resourceTokenCollectionId(properties.collectionId);
|
explorer.resourceTokenCollectionId(properties.collectionId);
|
||||||
if (properties.partitionKey) {
|
if (properties.partitionKey) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user