resolve master merge conflict

This commit is contained in:
hardiknai-techm
2021-04-19 17:52:28 +05:30
141 changed files with 12044 additions and 3407 deletions

View File

@@ -45,7 +45,6 @@ src/Definitions/jquery.d.ts
src/Definitions/plotly.js-cartesian-dist.d-min.ts src/Definitions/plotly.js-cartesian-dist.d-min.ts
src/Definitions/png.d.ts src/Definitions/png.d.ts
src/Definitions/svg.d.ts src/Definitions/svg.d.ts
src/Definitions/worker.d.ts
src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.test.ts
src/Explorer/ComponentRegisterer.ts src/Explorer/ComponentRegisterer.ts
src/Explorer/ContextMenuButtonFactory.ts src/Explorer/ContextMenuButtonFactory.ts
@@ -118,8 +117,6 @@ src/Explorer/Panes/AddCollectionPane.ts
src/Explorer/Panes/BrowseQueriesPane.ts src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/CassandraAddCollectionPane.ts src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ContextualPaneBase.ts src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/GraphStylingPane.ts src/Explorer/Panes/GraphStylingPane.ts
@@ -132,7 +129,6 @@ src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/AddTableEntityPane.ts src/Explorer/Panes/Tables/AddTableEntityPane.ts
src/Explorer/Panes/Tables/EditTableEntityPane.ts src/Explorer/Panes/Tables/EditTableEntityPane.ts
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
src/Explorer/Panes/Tables/QuerySelectPane.ts
src/Explorer/Panes/Tables/TableEntityPane.ts src/Explorer/Panes/Tables/TableEntityPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
@@ -248,7 +244,6 @@ src/Utils/QueryUtils.test.ts
src/applyExplorerBindings.ts src/applyExplorerBindings.ts
src/global.d.ts src/global.d.ts
src/setupTests.ts src/setupTests.ts
src/workers/upload/index.ts
src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx
src/Explorer/Controls/Accordion/AccordionComponent.tsx src/Explorer/Controls/Accordion/AccordionComponent.tsx
src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx
@@ -296,8 +291,6 @@ src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.t
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx

View File

@@ -3,7 +3,7 @@ module.exports = {
browser: true, browser: true,
es6: true, es6: true,
}, },
plugins: ["@typescript-eslint", "no-null", "prefer-arrow"], plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: { globals: {
Atomics: "readonly", Atomics: "readonly",
@@ -20,7 +20,7 @@ module.exports = {
overrides: [ overrides: [
{ {
files: ["**/*.tsx"], files: ["**/*.tsx"],
extends: ["plugin:react/recommended"], // TODO: Add react-hooks extends: ["plugin:react/recommended"],
plugins: ["react"], plugins: ["react"],
}, },
{ {
@@ -42,6 +42,8 @@ module.exports = {
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error", eqeqeq: "error",
"react/display-name": "off", "react/display-name": "off",
"react-hooks/rules-of-hooks": "warn", // TODO: error
"react-hooks/exhaustive-deps": "warn", // TODO: error
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {

1
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1 @@
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)

View File

@@ -69,7 +69,6 @@ module.exports = {
moduleNameMapper: { moduleNameMapper: {
"^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule", "^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule",
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule", "@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
"worker-loader": "<rootDir>/mockModule",
"office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes
"^dnd-core$": "dnd-core/dist/cjs", "^dnd-core$": "dnd-core/dist/cjs",
"^react-dnd$": "react-dnd/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs",

1062
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.9.0", "@azure/cosmos": "3.10.5",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.0.7",
@@ -25,7 +25,7 @@
"@nteract/iron-icons": "1.0.0", "@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0", "@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0", "@nteract/logos": "1.0.0",
"@nteract/markdown": "4.4.0", "@nteract/markdown": "4.6.0",
"@nteract/monaco-editor": "3.2.2", "@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0", "@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9", "@nteract/outputs": "3.0.9",
@@ -94,6 +94,7 @@
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"rxjs": "6.6.3", "rxjs": "6.6.3",
"sanitize-html": "2.3.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "3.1.0", "terser-webpack-plugin": "3.1.0",
@@ -122,10 +123,11 @@
"@types/prop-types": "15.5.8", "@types/prop-types": "15.5.8",
"@types/puppeteer": "5.4.3", "@types/puppeteer": "5.4.3",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.0", "@types/react": "17.0.3",
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.3",
"@types/react-notification-system": "0.2.39", "@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
"@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.1",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
@@ -178,8 +180,7 @@
"webpack": "4.43.0", "webpack": "4.43.0",
"webpack-bundle-analyzer": "3.6.1", "webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "3.3.10", "webpack-cli": "3.3.10",
"webpack-dev-server": "3.11.0", "webpack-dev-server": "3.11.0"
"worker-loader": "2.0.0"
}, },
"scripts": { "scripts": {
"start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js", "start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js",
@@ -202,8 +203,8 @@
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts", "build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js", "strict:find": "node ./strict-null-checks/find.js",
"autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js", "strict:add": "node ./strict-null-checks/auto-add.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks", "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts" "generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
}, },

View File

@@ -1,6 +1,7 @@
const express = require("express"); const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware"); const { createProxyMiddleware } = require("http-proxy-middleware");
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const fetch = require("node-fetch");
const api = createProxyMiddleware("/api", { const api = createProxyMiddleware("/api", {
target: "https://main.documentdb.ext.azure.com", target: "https://main.documentdb.ext.azure.com",
@@ -39,6 +40,29 @@ const app = express();
app.use(api); app.use(api);
app.use(proxy); app.use(proxy);
app.use(commit); app.use(commit);
app.get("/pull/:pr(\\d+)", (req, res) => {
const pr = req.params.pr;
const [, query] = req.originalUrl.split("?");
const search = new URLSearchParams(query);
fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
.then((response) => response.json())
.then(({ head: { ref, sha } }) => {
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
prUrl.hash = ref;
search.set("feature.pr", prUrl.href);
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
explorer.search = search.toString();
const portal = new URL("https://ms.portal.azure.com/");
portal.searchParams.set("dataExplorerSource", explorer.href);
return res.redirect(portal.href);
})
.catch(() => res.sendStatus(500));
});
app.listen(port, () => { app.listen(port, () => {
console.log(`Example app listening on port: ${port}`); console.log(`Example app listening on port: ${port}`);
}); });

View File

@@ -1,8 +1,658 @@
{ {
"name": "preview", "name": "cosmos-explorer-preview",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "cosmos-explorer-preview",
"version": "1.0.0",
"dependencies": {
"express": "^4.17.1",
"http-proxy-middleware": "^1.1.0",
"node-fetch": "^2.6.1"
}
},
"node_modules/@types/http-proxy": {
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz",
"integrity": "sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "14.14.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz",
"integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw=="
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"dependencies": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/camelcase": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
"engines": {
"node": ">=10"
}
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
"integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-middleware": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.1.0.tgz",
"integrity": "sha512-OnjU5vyVgcZVe2AjLJyMrk8YLNOC2lspCHirB5ldM+B/dwEfZ5bgVTrFyzE9R7xRWAP/i/FXtvIqKjTNEZBhBg==",
"dependencies": {
"@types/http-proxy": "^1.17.5",
"camelcase": "^6.2.0",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.1",
"is-plain-obj": "^3.0.0",
"micromatch": "^4.0.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-plain-obj": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
"engines": {
"node": ">=10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"dependencies": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz",
"integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.29",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz",
"integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==",
"dependencies": {
"mime-db": "1.46.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"engines": {
"node": ">=8.6"
}
},
"node_modules/proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"dependencies": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node_modules/serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"engines": {
"node": ">= 0.8"
}
}
},
"dependencies": { "dependencies": {
"@types/http-proxy": { "@types/http-proxy": {
"version": "1.17.5", "version": "1.17.5",
@@ -334,6 +984,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
}, },
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"on-finished": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",

View File

@@ -12,6 +12,7 @@
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"dependencies": { "dependencies": {
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^1.1.0" "http-proxy-middleware": "^1.1.0",
"node-fetch": "^2.6.1"
} }
} }

View File

@@ -75,7 +75,10 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
} }
} }
let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient { export function client(): Cosmos.CosmosClient {
if (_client) return _client;
const options: Cosmos.CosmosClientOptions = { const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey, key: userContext.masterKey,
@@ -89,5 +92,6 @@ export function client(): Cosmos.CosmosClient {
if (configContext.PROXY_PATH !== undefined) { if (configContext.PROXY_PATH !== undefined) {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }]; (options as any).plugins = [{ on: "request", plugin: requestPlugin }];
} }
return new Cosmos.CosmosClient(options); _client = new Cosmos.CosmosClient(options);
return _client;
} }

View File

@@ -48,32 +48,18 @@ export function sendCachedDataMessage<TResponseDataModel>(
} }
export function sendMessage(data: any): void { export function sendMessage(data: any): void {
if (canSendMessage()) { _sendMessage({
// We try to find data explorer window first, then fallback to current window signature: "pcIframe",
const portalChildWindow = getDataExplorerWindow(window) || window; data: data,
portalChildWindow.parent.postMessage( });
{
signature: "pcIframe",
data: data,
},
portalChildWindow.document.referrer || "*"
);
}
} }
export function sendReadyMessage(): void { export function sendReadyMessage(): void {
if (canSendMessage()) { _sendMessage({
// We try to find data explorer window first, then fallback to current window signature: "pcIframe",
const portalChildWindow = getDataExplorerWindow(window) || window; kind: "ready",
portalChildWindow.parent.postMessage( data: "ready",
{ });
signature: "pcIframe",
kind: "ready",
data: "ready",
},
portalChildWindow.document.referrer || "*"
);
}
} }
export function canSendMessage(): boolean { export function canSendMessage(): boolean {
@@ -89,3 +75,17 @@ export function runGarbageCollector() {
} }
}); });
} }
const _sendMessage = (message: any): void => {
if (canSendMessage()) {
// Portal window can receive messages from only child windows
const portalChildWindow = getDataExplorerWindow(window) || window;
if (portalChildWindow === window) {
// Current window is a child of portal, send message to portal window
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*");
} else {
// Current window is not a child of portal, send message to the child window instead (which is data explorer)
portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*");
}
}
};

View File

@@ -0,0 +1,39 @@
import { JSONObject, OperationResponse } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export const bulkCreateDocument = async (
collection: CollectionBase,
documents: JSONObject[]
): Promise<OperationResponse[]> => {
const clearMessage = logConsoleProgress(
`Executing ${documents.length} bulk operations for container ${collection.id()}`
);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.bulk(
documents.map((doc) => ({ operationType: "Create", resourceBody: doc })),
{ continueOnError: true }
);
const successCount = response.filter((r) => r.statusCode === 201).length;
const throttledCount = response.filter((r) => r.statusCode === 429).length;
logConsoleInfo(
`${
documents.length
} operations completed for container ${collection.id()}. ${successCount} operations succeeded. ${throttledCount} operations throttled`
);
return response;
} catch (error) {
handleError(error, "BulkCreateDocument", `Error bulk creating items for container ${collection.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,15 +1,15 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { userContext } from "../../UserContext";
import { handleError } from "../ErrorHandlingUtils";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; 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 { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
@@ -17,7 +17,6 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {
return await readCollectionsWithARM(databaseId); return await readCollectionsWithARM(databaseId);

View File

@@ -1,39 +1,37 @@
import { ContainerDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels"; import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { import { userContext } from "../../UserContext";
CreateUpdateOptions,
ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
MongoDBCollectionResource,
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { import {
createUpdateCassandraTable, createUpdateCassandraTable,
getCassandraTable, getCassandraTable,
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { import {
createUpdateGremlinGraph, createUpdateGremlinGraph,
getGremlinGraph, getGremlinGraph,
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { handleError } from "../ErrorHandlingUtils"; import {
ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function updateCollection( export async function updateCollection(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: Collection, newCollection: Partial<Collection>,
options: RequestOptions = {} options: RequestOptions = {}
): Promise<Collection> { ): Promise<Collection> {
let collection: Collection; let collection: Collection;
@@ -43,7 +41,6 @@ export async function updateCollection(
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
@@ -69,7 +66,7 @@ export async function updateCollection(
async function updateCollectionWithARM( async function updateCollectionWithARM(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: Collection newCollection: Partial<Collection>
): Promise<Collection> { ): Promise<Collection> {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
@@ -85,6 +82,15 @@ async function updateCollectionWithARM(
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Table: case DefaultAccountExperienceType.Table:
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
default: default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`); throw new Error(`Unsupported default experience type: ${defaultExperience}`);
} }
@@ -96,7 +102,7 @@ async function updateSqlContainer(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Collection newCollection: Partial<Collection>
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -115,35 +121,26 @@ async function updateSqlContainer(
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`); throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
} }
export async function updateMongoDBCollectionThroughRP( export async function updateMongoDBCollection(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: MongoDBCollectionResource, subscriptionId: string,
updateOptions?: CreateUpdateOptions resourceGroup: string,
): Promise<MongoDBCollectionResource> { accountName: string,
const subscriptionId = userContext.subscriptionId; newCollection: Partial<Collection>
const resourceGroup = userContext.resourceGroup; ): Promise<Collection> {
const accountName = userContext.databaseAccount.name;
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
const updateParams: MongoDBCollectionCreateUpdateParameters = { getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
properties: {
resource: newCollection,
options: updateOptions,
},
};
const updateResponse = await createUpdateMongoDBCollection( const updateResponse = await createUpdateMongoDBCollection(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
databaseId, databaseId,
collectionId, collectionId,
updateParams getResponse as MongoDBCollectionCreateUpdateParameters
); );
return updateResponse && (updateResponse.properties.resource as Collection);
return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource);
} }
throw new Error( throw new Error(
@@ -157,7 +154,7 @@ async function updateCassandraTable(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Collection newCollection: Partial<Collection>
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -184,7 +181,7 @@ async function updateGremlinGraph(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Collection newCollection: Partial<Collection>
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -208,7 +205,7 @@ async function updateTable(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Collection newCollection: Partial<Collection>
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId); const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {

View File

@@ -121,6 +121,10 @@ export interface ISchemaRequest {
} }
export interface Collection extends Resource { export interface Collection extends Resource {
// Only in Mongo collections loaded via ARM
shardKey?: {
[key: string]: string;
};
defaultTtl?: number; defaultTtl?: number;
indexingPolicy?: IndexingPolicy; indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey; partitionKey?: PartitionKey;

View File

@@ -15,7 +15,6 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger"; import Trigger from "../Explorer/Tree/Trigger";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { SelfServeType } from "../SelfServe/SelfServeUtils"; import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels"; import * as DataModels from "./DataModels";
import { SubscriptionType } from "./SubscriptionType"; import { SubscriptionType } from "./SubscriptionType";
@@ -23,6 +22,14 @@ export interface TokenProvider {
getAuthHeader(): Promise<Headers>; getAuthHeader(): Promise<Headers>;
} }
export interface UploadDetailsRecord {
fileName: string;
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}
export interface QueryResultsMetadata { export interface QueryResultsMetadata {
hasMoreResults: boolean; hasMoreResults: boolean;
firstItemIndex: number; firstItemIndex: number;
@@ -174,7 +181,7 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<UploadDetails>; uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
getLabel(): string; getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>; getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
@@ -269,7 +276,6 @@ export interface TabOptions {
tabKind: CollectionTabKind; tabKind: CollectionTabKind;
title: string; title: string;
tabPath: string; tabPath: string;
isActive: ko.Observable<boolean>;
hashLocation: string; hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void; onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>; isTabsContentExpanded?: ko.Observable<boolean>;
@@ -390,6 +396,9 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string; dataExplorerVersion?: string;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
features?: {
[key: string]: string;
};
} }
export interface SelfServeFrameInputs { export interface SelfServeFrameInputs {

View File

@@ -1,7 +0,0 @@
declare module "worker-loader!*" {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}

View File

@@ -73,10 +73,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true); expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
}); });
it("should register delete-collection-confirmation-pane component", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
});
it("should register graph-new-vertex-pane component", () => { it("should register graph-new-vertex-pane component", () => {
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true); expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
}); });

View File

@@ -57,16 +57,11 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
// Panes // Panes
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent()); ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register(
"delete-collection-confirmation-pane",
new PaneComponents.DeleteCollectionConfirmationPaneComponent()
);
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent()); ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());

View File

@@ -55,7 +55,7 @@ export class ResourceTreeContextMenuButtonFactory {
selectedCollection: ViewModels.Collection selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] { ): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = []; const items: TreeNodeMenuItem[] = [];
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) { if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null), onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null),
@@ -80,7 +80,7 @@ export class ResourceTreeContextMenuButtonFactory {
}); });
} }
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) { if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({ items.push({
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
onClick: () => { onClick: () => {
@@ -123,7 +123,7 @@ export class ResourceTreeContextMenuButtonFactory {
container: Explorer, container: Explorer,
storedProcedure: StoredProcedure storedProcedure: StoredProcedure
): TreeNodeMenuItem[] { ): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
return []; return [];
} }
@@ -137,7 +137,7 @@ export class ResourceTreeContextMenuButtonFactory {
} }
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] { public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
return []; return [];
} }
@@ -154,7 +154,7 @@ export class ResourceTreeContextMenuButtonFactory {
container: Explorer, container: Explorer,
userDefinedFunction: UserDefinedFunction userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] { ): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
return []; return [];
} }

View File

@@ -21,18 +21,18 @@ import {
Text, Text,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { HttpStatusCodes } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import Explorer from "../../Explorer";
import { Dialog, DialogProps } from "../Dialog"; import { Dialog, DialogProps } from "../Dialog";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less";
import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent"; import { CodeOfConductComponent } from "./CodeOfConductComponent";
import "./GalleryViewerComponent.less";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -138,11 +138,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
key: SortBy.MostRecent, key: SortBy.MostRecent,
text: GalleryViewerComponent.mostRecentText, text: GalleryViewerComponent.mostRecentText,
}, },
{
key: SortBy.MostFavorited,
text: GalleryViewerComponent.mostFavoritedText,
},
]; ];
this.sortingOptions.push({
key: SortBy.MostFavorited,
text: GalleryViewerComponent.mostFavoritedText,
});
this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false); this.loadTabContent(this.state.selectedTab, this.state.searchText, this.state.sortBy, false);
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
@@ -654,7 +654,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
private onRenderCell = (data?: IGalleryItem): JSX.Element => { private onRenderCell = (data?: IGalleryItem): JSX.Element => {
const isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined; const isFavorite =
this.props.container && this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
const props: GalleryCardComponentProps = { const props: GalleryCardComponentProps = {
data, data,
isFavorite, isFavorite,

View File

@@ -1,11 +1,11 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import ko from "knockout"; import ko from "knockout";
import React from "react"; import React from "react";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
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 { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent"; import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
@@ -23,13 +23,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
changeFeedPolicy: undefined, changeFeedPolicy: undefined,
analyticalStorageTtl: undefined, analyticalStorageTtl: undefined,
geospatialConfig: undefined, geospatialConfig: undefined,
} as DataModels.Collection),
updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({
id: undefined,
shardKey: undefined,
indexes: [], indexes: [],
analyticalStorageTtl: undefined, }),
} as MongoDBCollectionResource),
})); }));
jest.mock("../../../Common/dataAccess/updateOffer", () => ({ jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
@@ -44,7 +39,6 @@ describe("SettingsComponent", () => {
tabPath: "", tabPath: "",
node: undefined, node: undefined,
hashLocation: "settings", hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
}), }),
}; };
@@ -113,7 +107,13 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false); expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
const newContainer = new Explorer(); const newContainer = new Explorer();
newContainer.isPreferredApiCassandra = ko.computed(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DataModels.DatabaseAccount,
});
const newCollection = { ...collection }; const newCollection = { ...collection };
newCollection.container = newContainer; newCollection.container = newContainer;
@@ -193,7 +193,6 @@ describe("SettingsComponent", () => {
}; };
await settingsComponentInstance.onSaveClick(); await settingsComponentInstance.onSaveClick();
expect(updateCollection).toBeCalled(); expect(updateCollection).toBeCalled();
expect(updateMongoDBCollectionThroughRP).toBeCalled();
expect(updateOffer).toBeCalled(); expect(updateOffer).toBeCalled();
}); });

View File

@@ -6,7 +6,7 @@ import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
@@ -137,7 +137,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.offer = this.collection?.offer(); this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl(); this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor = this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB(); this.container && userContext.apiType !== "Cassandra" && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -299,7 +299,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected; this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean => public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection); this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean => public hasConflictResolution = (): boolean =>
this.container?.databaseAccount && this.container?.databaseAccount &&
@@ -782,12 +782,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) { if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
try { try {
const newMongoIndexes = this.getMongoIndexesToSave(); const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection: MongoDBCollectionResource = { const newMongoCollection = {
...this.mongoDBCollectionResource, ...this.mongoDBCollectionResource,
indexes: newMongoIndexes, indexes: newMongoIndexes,
}; };
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP( this.mongoDBCollectionResource = await updateCollection(
this.collection.databaseId, this.collection.databaseId,
this.collection.id(), this.collection.id(),
newMongoCollection newMongoCollection

View File

@@ -1,14 +1,13 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent"; import { DatabaseAccount } from "../../../../Contracts/DataModels";
import { container, collection } from "../TestUtils"; import { updateUserContext } from "../../../../UserContext";
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
import ko from "knockout";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import { ChangeFeedPolicyState, GeospatialConfigType, TtlOff, TtlOn, TtlOnNoDefault, TtlType } from "../SettingsUtils";
import { collection, container } from "../TestUtils";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
describe("SubSettingsComponent", () => { describe("SubSettingsComponent", () => {
container.isPreferredApiDocumentDB = ko.computed(() => true);
const baseProps: SubSettingsComponentProps = { const baseProps: SubSettingsComponentProps = {
collection: collection, collection: collection,
container: container, container: container,
@@ -106,8 +105,13 @@ describe("SubSettingsComponent", () => {
it("partitionKey not visible", () => { it("partitionKey not visible", () => {
const newContainer = new Explorer(); const newContainer = new Explorer();
updateUserContext({
newContainer.isPreferredApiCassandra = ko.computed(() => true); databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const props = { ...baseProps, container: newContainer }; const props = { ...baseProps, container: newContainer };
const subSettingsComponent = new SubSettingsComponent(props); const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false); expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false);

View File

@@ -1,28 +1,38 @@
import {
ChoiceGroup,
IChoiceGroupOption,
Label,
Link,
MessageBar,
Stack,
Text,
TextField,
} from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { import { userContext } from "../../../../UserContext";
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
isDirty,
IsComponentDirtyResult,
TtlOn,
TtlOff,
TtlOnNoDefault,
getSanitizedInputValue,
} from "../SettingsUtils";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react";
import { import {
getTextFieldStyles,
changeFeedPolicyToolTip, changeFeedPolicyToolTip,
getChoiceGroupStyles,
getTextFieldStyles,
messageBarStyles,
subComponentStackProps, subComponentStackProps,
titleAndInputStackProps, titleAndInputStackProps,
getChoiceGroupStyles,
ttlWarning, ttlWarning,
messageBarStyles,
} from "../SettingsRenderUtils"; } from "../SettingsRenderUtils";
import {
ChangeFeedPolicyState,
GeospatialConfigType,
getSanitizedInputValue,
IsComponentDirtyResult,
isDirty,
TtlOff,
TtlOn,
TtlOnNoDefault,
TtlType,
} from "../SettingsUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface SubSettingsComponentProps { export interface SubSettingsComponentProps {
@@ -60,17 +70,15 @@ export interface SubSettingsComponentProps {
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> { export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
private shouldCheckComponentIsDirty = true; private shouldCheckComponentIsDirty = true;
private ttlVisible: boolean;
private geospatialVisible: boolean; private geospatialVisible: boolean;
private partitionKeyValue: string; private partitionKeyValue: string;
private partitionKeyName: string; private partitionKeyName: string;
constructor(props: SubSettingsComponentProps) { constructor(props: SubSettingsComponentProps) {
super(props); super(props);
this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false; this.geospatialVisible = userContext.apiType === "SQL";
this.geospatialVisible = this.props.container.isPreferredApiDocumentDB();
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty; this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key"; this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
} }
componentDidMount(): void { componentDidMount(): void {
@@ -170,39 +178,51 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
): void => ): void =>
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]); this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
private getTtlComponent = (): JSX.Element => ( private getTtlComponent = (): JSX.Element =>
<Stack {...titleAndInputStackProps}> userContext.apiType === "Mongo" ? (
<ChoiceGroup <MessageBar
id="timeToLive" messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
label="Time to Live" styles={{ text: { fontSize: 14 } }}
selectedKey={this.props.timeToLive} >
options={this.ttlChoiceGroupOptions} To enable time-to-live (TTL) for your collection/documents,
onChange={this.onTtlChange} <Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank">
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)} create a TTL index
/> </Link>
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && ( .
<MessageBar </MessageBar>
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }} ) : (
styles={messageBarStyles} <Stack {...titleAndInputStackProps}>
> <ChoiceGroup
{ttlWarning} id="timeToLive"
</MessageBar> label="Time to Live"
)} selectedKey={this.props.timeToLive}
{this.props.timeToLive === TtlType.On && ( options={this.ttlChoiceGroupOptions}
<TextField onChange={this.onTtlChange}
id="timeToLiveSeconds" styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
/> />
)} {isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
</Stack> <MessageBar
); messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
styles={messageBarStyles}
>
{ttlWarning}
</MessageBar>
)}
{this.props.timeToLive === TtlType.On && (
<TextField
id="timeToLiveSeconds"
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
/>
)}
</Stack>
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [ private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", disabled: true }, { key: TtlType.Off, text: "Off", disabled: true },
@@ -300,7 +320,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public getPartitionKeyVisible = (): boolean => { public getPartitionKeyVisible = (): boolean => {
if ( if (
this.props.container.isPreferredApiCassandra() || userContext.apiType === "Cassandra" ||
this.props.container.isPreferredApiTable() || this.props.container.isPreferredApiTable() ||
!this.props.collection.partitionKeyProperty || !this.props.collection.partitionKeyProperty ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey) (this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
@@ -315,7 +335,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
{this.ttlVisible && this.getTtlComponent()} {userContext.apiType !== "Cassandra" && this.getTtlComponent()}
{this.geospatialVisible && this.getGeoSpatialComponent()} {this.geospatialVisible && this.getGeoSpatialComponent()}

View File

@@ -115,21 +115,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -220,27 +205,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -531,21 +495,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -614,9 +563,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -656,27 +602,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -868,21 +793,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -973,27 +883,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -1284,21 +1173,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -1367,9 +1241,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -1409,27 +1280,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -1634,21 +1484,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -1739,27 +1574,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -2050,21 +1864,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -2133,9 +1932,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -2175,27 +1971,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -2387,21 +2162,6 @@ exports[`SettingsComponent renders 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -2492,27 +2252,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -2803,21 +2542,6 @@ exports[`SettingsComponent renders 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -2886,9 +2610,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -2928,27 +2649,6 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],

View File

@@ -4,6 +4,7 @@ jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -13,11 +14,8 @@ describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => { const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer; const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]); explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false); explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false);
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false); explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
explorerStub.findDatabaseWithId = () => database; explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve(); explorerStub.refreshAllDatabases = () => Q.resolve();
@@ -31,7 +29,7 @@ describe("ContainerSampleGenerator", () => {
it("should insert documents for sql API account", async () => { it("should insert documents for sql API account", async () => {
const sampleCollectionId = "SampleCollection"; const sampleCollectionId = "SampleCollection";
const sampleDatabaseId = "SampleDB"; const sampleDatabaseId = "SampleDB";
updateUserContext({});
const sampleData = { const sampleData = {
databaseId: sampleDatabaseId, databaseId: sampleDatabaseId,
offerThroughput: 400, offerThroughput: 400,
@@ -66,7 +64,7 @@ describe("ContainerSampleGenerator", () => {
database.findCollectionWithId = () => collection; database.findCollectionWithId = () => collection;
const explorerStub = createExplorerStub(database); const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true);
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
@@ -116,7 +114,13 @@ describe("ContainerSampleGenerator", () => {
collection.databaseId = database.id(); collection.databaseId = database.id();
const explorerStub = createExplorerStub(database); const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
@@ -125,31 +129,45 @@ describe("ContainerSampleGenerator", () => {
}); });
it("should not create any sample for Mongo API account", async () => { it("should not create any sample for Mongo API account", async () => {
const experience = "not supported api"; const experience = "Sample generation not supported for this API Mongo";
const explorerStub = createExplorerStub(undefined); const explorerStub = createExplorerStub(undefined);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => true); updateUserContext({
explorerStub.defaultExperience = ko.observable<string>(experience); databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
// Rejects with error that contains experience // Rejects with error that contains experience
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });
it("should not create any sample for Table API account", async () => { it("should not create any sample for Table API account", async () => {
const experience = "not supported api"; const experience = "Sample generation not supported for this API Tables";
const explorerStub = createExplorerStub(undefined); const explorerStub = createExplorerStub(undefined);
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => true); updateUserContext({
explorerStub.defaultExperience = ko.observable<string>(experience); databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
// Rejects with error that contains experience // Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });
it("should not create any sample for Cassandra API account", async () => { it("should not create any sample for Cassandra API account", async () => {
const experience = "not supported api"; const experience = "Sample generation not supported for this API Cassandra";
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const explorerStub = createExplorerStub(undefined); const explorerStub = createExplorerStub(undefined);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => true);
explorerStub.defaultExperience = ko.observable<string>(experience);
// Rejects with error that contains experience // Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });

View File

@@ -1,12 +1,12 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import GraphTab from ".././Tabs/GraphTab";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import GraphTab from ".././Tabs/GraphTab";
import Explorer from "../Explorer";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
interface SampleDataFile extends DataModels.CreateCollectionParams { interface SampleDataFile extends DataModels.CreateCollectionParams {
data: any[]; data: any[];
@@ -23,16 +23,16 @@ export class ContainerSampleGenerator {
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> { public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
const generator = new ContainerSampleGenerator(container); const generator = new ContainerSampleGenerator(container);
let dataFileContent: any; let dataFileContent: any;
if (container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
dataFileContent = await import( dataFileContent = await import(
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json" /* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
); );
} else if (container.isPreferredApiDocumentDB()) { } else if (userContext.apiType === "SQL") {
dataFileContent = await import( dataFileContent = await import(
/* webpackChunkName: "sqlSampleJsonData" */ "../../../sampleData/sqlSampleData.json" /* webpackChunkName: "sqlSampleJsonData" */ "../../../sampleData/sqlSampleData.json"
); );
} else { } else {
return Promise.reject(`Sample generation not supported for this API ${container.defaultExperience()}`); return Promise.reject(`Sample generation not supported for this API ${userContext.apiType}`);
} }
generator.setData(dataFileContent); generator.setData(dataFileContent);
@@ -73,7 +73,7 @@ export class ContainerSampleGenerator {
} }
const promises: Q.Promise<any>[] = []; const promises: Q.Promise<any>[] = [];
if (this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
// For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries // For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries
// (e.g. adding edge requires vertices to be present) // (e.g. adding edge requires vertices to be present)
const queries: string[] = this.sampleDataFile.data; const queries: string[] = this.sampleDataFile.data;

View File

@@ -1,4 +1,5 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
@@ -56,6 +57,6 @@ export class DataSamplesUtil {
} }
public isSampleContainerCreationSupported(): boolean { public isSampleContainerCreationSupported(): boolean {
return this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph(); return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
} }
} }

View File

@@ -36,6 +36,7 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as PricingUtils from "../Utils/PricingUtils";
import * as ComponentRegisterer from "./ComponentRegisterer"; import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
@@ -48,11 +49,10 @@ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/Noteboo
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane"; import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { AddDatabasePane } from "./Panes/AddDatabasePane"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel"; import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel"; import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel"; import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
@@ -65,9 +65,10 @@ import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase"; import TabsBase from "./Tabs/TabsBase";
@@ -78,8 +79,6 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
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
@@ -93,6 +92,7 @@ export interface ExplorerParams {
closeSidePanel: () => void; closeSidePanel: () => void;
closeDialog: () => void; closeDialog: () => void;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
tabsManager: TabsManager;
} }
export default class Explorer { export default class Explorer {
@@ -116,26 +116,12 @@ export default class Explorer {
* Use userContext.apiType instead * Use userContext.apiType instead
* */ * */
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "SQL"
* */
public isPreferredApiDocumentDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra"
* */
public isPreferredApiCassandra: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo" * Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */ * */
public isPreferredApiMongoDB: ko.Computed<boolean>; public isPreferredApiMongoDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
* */
public isPreferredApiGraph: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables" * Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
@@ -188,12 +174,11 @@ export default class Explorer {
public tabsManager: TabsManager; public tabsManager: TabsManager;
// Contextual panes // Contextual panes
public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane; public addCollectionPane: AddCollectionPane;
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
public graphStylingPane: GraphStylingPane; public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane; public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane; public editTableEntityPane: EditTableEntityPane;
public querySelectPane: QuerySelectPane;
public newVertexPane: NewVertexPane; public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public stringInputPane: StringInputPane; public stringInputPane: StringInputPane;
@@ -420,20 +405,6 @@ export default class Explorer {
}); });
}); });
this.isPreferredApiDocumentDB = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.DocumentDB.toLowerCase();
});
this.isPreferredApiCassandra = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase();
});
this.isPreferredApiGraph = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase();
});
this.isPreferredApiTable = ko.computed(() => { this.isPreferredApiTable = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase(); return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase();
@@ -497,7 +468,9 @@ export default class Explorer {
this.isHostedDataExplorerEnabled = ko.computed<boolean>( this.isHostedDataExplorerEnabled = ko.computed<boolean>(
() => () =>
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() configContext.platform === Platform.Portal &&
!this.isRunningOnNationalCloud() &&
userContext.apiType !== "Gremlin"
); );
this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2); this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2);
this.selectedDatabaseId = ko.computed<string>(() => { this.selectedDatabaseId = ko.computed<string>(() => {
@@ -521,16 +494,16 @@ export default class Explorer {
} }
}); });
this.addCollectionPane = new AddCollectionPane({ this.addDatabasePane = new AddDatabasePane({
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), id: "adddatabasepane",
id: "addcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
container: this, container: this,
}); });
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({ this.addCollectionPane = new AddCollectionPane({
id: "deletecollectionconfirmationpane", isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
id: "addcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
container: this, container: this,
@@ -557,13 +530,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.querySelectPane = new QuerySelectPane({
id: "queryselectpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.newVertexPane = new NewVertexPane({ this.newVertexPane = new NewVertexPane({
id: "newvertexpane", id: "newvertexpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -592,21 +558,26 @@ export default class Explorer {
container: this, container: this,
}); });
this.tabsManager = new TabsManager(); this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) {
this.selectedNode(undefined);
this.onUpdateTabsButtons([]);
}
});
this._panes = [ this._panes = [
this.addDatabasePane,
this.addCollectionPane, this.addCollectionPane,
this.deleteCollectionConfirmationPane,
this.graphStylingPane, this.graphStylingPane,
this.addTableEntityPane, this.addTableEntityPane,
this.editTableEntityPane, this.editTableEntityPane,
this.querySelectPane,
this.newVertexPane, this.newVertexPane,
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.stringInputPane, this.stringInputPane,
this.setupNotebooksPane, this.setupNotebooksPane,
]; ];
//this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
document.addEventListener( document.addEventListener(
@@ -634,8 +605,6 @@ export default class Explorer {
this.addCollectionPane.collectionWithThroughputInSharedTitle( this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this container" "Provision dedicated throughput for this container"
); );
this.deleteCollectionConfirmationPane.title("Delete Container");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
this.refreshTreeTitle("Refresh containers"); this.refreshTreeTitle("Refresh containers");
break; break;
case "Mongo": case "Mongo":
@@ -662,8 +631,6 @@ export default class Explorer {
this.addCollectionPane.title("Add Graph"); this.addCollectionPane.title("Add Graph");
this.addCollectionPane.collectionIdTitle("Graph id"); this.addCollectionPane.collectionIdTitle("Graph id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.deleteCollectionConfirmationPane.title("Delete Graph");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id");
this.refreshTreeTitle("Refresh graphs"); this.refreshTreeTitle("Refresh graphs");
break; break;
case "Tables": case "Tables":
@@ -679,8 +646,6 @@ export default class Explorer {
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Entity"); this.addTableEntityPane.title("Add Table Entity");
this.editTableEntityPane.title("Edit Table Entity"); this.editTableEntityPane.title("Edit Table Entity");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new TablesAPIDataClient(); this.tableDataClient = new TablesAPIDataClient();
break; break;
case "Cassandra": case "Cassandra":
@@ -696,8 +661,6 @@ export default class Explorer {
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Row"); this.addTableEntityPane.title("Add Table Row");
this.editTableEntityPane.title("Edit Table Row"); this.editTableEntityPane.title("Edit Table Row");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
} }
@@ -915,10 +878,8 @@ export default class Explorer {
// TODO: Refactor // TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer(); const deferred: Q.Deferred<any> = Q.defer();
this._setLoadingStatusText("Fetching databases...");
readDatabases().then( readDatabases().then(
(databases: DataModels.Database[]) => { (databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabases, Action.LoadDatabases,
{ {
@@ -931,20 +892,16 @@ export default class Explorer {
this.addDatabasesToList(deltaDatabases.toAdd); this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete); this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode); this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
() => { () => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve(); deferred.resolve();
}, },
(reason) => { (reason) => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason); deferred.reject(reason);
} }
); );
}, },
(error) => { (error) => {
this._setLoadingStatusText("Failed to fetch databases.");
deferred.reject(error); deferred.reject(error);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
@@ -1020,7 +977,7 @@ export default class Explorer {
// Facade // Facade
public provideFeedbackEmail = () => { public provideFeedbackEmail = () => {
window.open(Constants.Urls.feedbackEmail, "_self"); window.open(Constants.Urls.feedbackEmail, "_blank");
}; };
public async getArcadiaToken(): Promise<string> { public async getArcadiaToken(): Promise<string> {
@@ -1291,49 +1248,6 @@ export default class Explorer {
: this.selectedNode().collection) as ViewModels.Collection; : this.selectedNode().collection) as ViewModels.Collection;
} }
// TODO: Refactor below methods, minimize dependencies and add unit tests where necessary
public findSelectedStoredProcedure(): StoredProcedure {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => {
const openedSprocTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures,
(tab) => tab.node && tab.node.rid === storedProcedure.rid
);
return (
storedProcedure.rid === this.selectedNode().rid ||
(!!openedSprocTab && openedSprocTab.length > 0 && openedSprocTab[0].isActive())
);
});
}
public findSelectedUDF(): UserDefinedFunction {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => {
const openedUdfTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions,
(tab) => tab.node && tab.node.rid === userDefinedFunction.rid
);
return (
userDefinedFunction.rid === this.selectedNode().rid ||
(!!openedUdfTab && openedUdfTab.length > 0 && openedUdfTab[0].isActive())
);
});
}
public findSelectedTrigger(): Trigger {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.triggers(), (trigger: Trigger) => {
const openedTriggerTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Triggers,
(tab) => tab.node && tab.node.rid === trigger.rid
);
return (
trigger.rid === this.selectedNode().rid ||
(!!openedTriggerTab && openedTriggerTab.length > 0 && openedTriggerTab[0].isActive())
);
});
}
public closeAllPanes(): void { public closeAllPanes(): void {
this._panes.forEach((pane: ContextualPaneBase) => pane.close()); this._panes.forEach((pane: ContextualPaneBase) => pane.close());
} }
@@ -1662,7 +1576,6 @@ export default class Explorer {
collection: null, collection: null,
masterKey: userContext.masterKey || "", masterKey: userContext.masterKey || "",
hashLocation: "notebooks", hashLocation: "notebooks",
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null, onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons, onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2068,7 +1981,6 @@ export default class Explorer {
tabPath: title, tabPath: title,
collection: null, collection: null,
hashLocation: hashLocation, hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null, onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons, onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2173,7 +2085,7 @@ export default class Explorer {
} }
public onNewCollectionClicked(): void { public onNewCollectionClicked(): void {
if (this.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
this.cassandraAddCollectionPane.open(); this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableReactPane) { } else if (userContext.features.enableReactPane) {
this.openAddCollectionPanel(); this.openAddCollectionPanel();
@@ -2216,32 +2128,6 @@ export default class Explorer {
} }
} }
private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") {
if (!text) {
return;
}
const loadingText = document.getElementById("explorerLoadingStatusText");
if (!loadingText) {
Logger.logError(
"getElementById('explorerLoadingStatusText') failed to find element",
"Explorer/_setLoadingStatusText"
);
return;
}
loadingText.innerHTML = text;
const loadingTitle = document.getElementById("explorerLoadingStatusTitle");
if (!loadingTitle) {
Logger.logError(
"getElementById('explorerLoadingStatusTitle') failed to find element",
"Explorer/_setLoadingStatusText"
);
} else {
loadingTitle.innerHTML = title;
}
}
private _openSetupNotebooksPaneForQuickstart(): void { private _openSetupNotebooksPaneForQuickstart(): void {
const title = "Enable Notebooks (Preview)"; const title = "Enable Notebooks (Preview)";
const description = const description =
@@ -2309,16 +2195,15 @@ export default class Explorer {
} }
public openDeleteCollectionConfirmationPane(): void { public openDeleteCollectionConfirmationPane(): void {
userContext.features.enableKOPanel let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience);
? this.deleteCollectionConfirmationPane.open() this.openSidePanel(
: this.openSidePanel( "Delete " + collectionName,
"Delete Collection", <DeleteCollectionConfirmationPanel
<DeleteCollectionConfirmationPanel explorer={this}
explorer={this} collectionName={collectionName}
closePanel={() => this.closeSidePanel()} closePanel={this.closeSidePanel}
openNotificationConsole={() => this.expandConsole()} />
/> );
);
} }
public openDeleteDatabaseConfirmationPane(): void { public openDeleteDatabaseConfirmationPane(): void {
@@ -2341,10 +2226,14 @@ export default class Explorer {
this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />); this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />);
} }
public openExecuteSprocParamsPanel(): void { public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
this.openSidePanel( this.openSidePanel(
"Input parameters", "Input parameters",
<ExecuteSprocParamsPanel explorer={this} closePanel={() => this.closeSidePanel()} /> <ExecuteSprocParamsPanel
explorer={this}
storedProcedure={storedProcedure}
closePanel={() => this.closeSidePanel()}
/>
); );
} }
@@ -2389,4 +2278,11 @@ export default class Explorer {
/> />
); );
} }
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
this.openSidePanel(
"Select Column",
<TableQuerySelectPanel explorer={this} closePanel={this.closeSidePanel} queryViewModel={queryViewModal} />
);
}
} }

View File

@@ -1,6 +1,6 @@
import { NeighborVertexBasicInfo } from "./GraphExplorer";
import * as GraphData from "./GraphData";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import * as GraphData from "./GraphData";
import { NeighborVertexBasicInfo } from "./GraphExplorer";
interface JoinArrayMaxCharOutput { interface JoinArrayMaxCharOutput {
result: string; // string output result: string; // string output
@@ -13,9 +13,9 @@ interface EdgePropertyType {
inV?: string; inV?: string;
} }
export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string { export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => {
return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`; return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`;
} };
/** /**
* Collect all edges from this node * Collect all edges from this node
@@ -23,11 +23,11 @@ export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
* @param graphData * @param graphData
* @param newNodes (optional) object describing new nodes encountered * @param newNodes (optional) object describing new nodes encountered
*/ */
export function createEdgesfromNode( export const createEdgesfromNode = (
vertex: GraphData.GremlinVertex, vertex: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>, graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
newNodes?: { [id: string]: boolean } newNodes?: { [id: string]: boolean }
): void { ): void => {
if (Object.prototype.hasOwnProperty.call(vertex, "outE")) { if (Object.prototype.hasOwnProperty.call(vertex, "outE")) {
const outE = vertex.outE; const outE = vertex.outE;
for (const label in outE) { for (const label in outE) {
@@ -66,7 +66,7 @@ export function createEdgesfromNode(
}); });
} }
} }
} };
/** /**
* From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'". * From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'".
@@ -75,7 +75,7 @@ export function createEdgesfromNode(
* @param maxSize * @param maxSize
* @return * @return
*/ */
export function getLimitedArrayString(array: string[], maxSize: number): JoinArrayMaxCharOutput { export const getLimitedArrayString = (array: string[], maxSize: number): JoinArrayMaxCharOutput => {
if (!array || array.length === 0 || array[0].length + 2 > maxSize) { if (!array || array.length === 0 || array[0].length + 2 > maxSize) {
return { result: "", consumedCount: 0 }; return { result: "", consumedCount: 0 };
} }
@@ -96,16 +96,16 @@ export function getLimitedArrayString(array: string[], maxSize: number): JoinArr
result: output, result: output,
consumedCount: i + 1, consumedCount: i + 1,
}; };
} };
export function createFetchEdgePairQuery( export const createFetchEdgePairQuery = (
outE: boolean, outE: boolean,
pkid: string, pkid: string,
excludedEdgeIds: string[], excludedEdgeIds: string[],
startIndex: number, startIndex: number,
pageSize: number, pageSize: number,
withoutStepArgMaxLenght: number withoutStepArgMaxLenght: number
): string { ): string => {
let gremlinQuery: string; let gremlinQuery: string;
if (excludedEdgeIds.length > 0) { if (excludedEdgeIds.length > 0) {
// build a string up to max char // build a string up to max char
@@ -128,15 +128,15 @@ export function createFetchEdgePairQuery(
}().as('v').select('e', 'v')`; }().as('v').select('e', 'v')`;
} }
return gremlinQuery; return gremlinQuery;
} };
/** /**
* Trim graph * Trim graph
*/ */
export function trimGraph( export const trimGraph = (
currentRoot: GraphData.GremlinVertex, currentRoot: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
) { ): void => {
const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId); const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId);
graphData.unloadAllVertices(importantNodes); graphData.unloadAllVertices(importantNodes);
@@ -144,32 +144,32 @@ export function trimGraph(
$.each(graphData.ids, (index: number, id: string) => { $.each(graphData.ids, (index: number, id: string) => {
graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1; graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1;
}); });
} };
export function addRootChildToGraph( export const addRootChildToGraph = (
root: GraphData.GremlinVertex, root: GraphData.GremlinVertex,
child: GraphData.GremlinVertex, child: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
) { ): void => {
child._ancestorsId = (root._ancestorsId || []).concat([root.id]); child._ancestorsId = (root._ancestorsId || []).concat([root.id]);
graphData.addVertex(child); graphData.addVertex(child);
createEdgesfromNode(child, graphData); createEdgesfromNode(child, graphData);
graphData.addNeighborInfo(child); graphData.addNeighborInfo(child);
} };
/** /**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now.
* @param value * @param value
*/ */
export function escapeDoubleQuotes(value: string): string { export const escapeDoubleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/"/g, '\\"'); return value === undefined ? value : value.replace(/"/g, '\\"');
} };
/** /**
* Surround with double-quotes if val is a string. * Surround with double-quotes if val is a string.
* @param val * @param val
*/ */
export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string { export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string => {
switch (ip.type) { switch (ip.type) {
case "number": case "number":
case "boolean": case "boolean":
@@ -179,12 +179,12 @@ export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
default: default:
return `"${escapeDoubleQuotes(ip.value as string)}"`; return `"${escapeDoubleQuotes(ip.value as string)}"`;
} }
} };
/** /**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now.
* @param value * @param value
*/ */
export function escapeSingleQuotes(value: string): string { export const escapeSingleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/'/g, "\\'"); return value === undefined ? value : value.replace(/'/g, "\\'");
} };

View File

@@ -4,15 +4,15 @@
* and update any knockout observables passed from the parent. * and update any knockout observables passed from the parent.
*/ */
import * as ko from "knockout"; import * as ko from "knockout";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
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 CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import * as CommandBarUtil from "./CommandBarUtil"; import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
export class CommandBarComponentAdapter implements ReactAdapter { export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>; public parameters: ko.Observable<number>;
@@ -23,17 +23,14 @@ export class CommandBarComponentAdapter implements ReactAdapter {
constructor(container: Explorer) { constructor(container: Explorer) {
this.container = container; this.container = container;
this.tabsButtons = []; this.tabsButtons = [];
this.isNotebookTabActive = ko.computed(() => this.isNotebookTabActive = ko.computed(
container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2) () => container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2
); );
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates // These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [ const toWatch = [
container.isPreferredApiTable, container.isPreferredApiTable,
container.isPreferredApiMongoDB, container.isPreferredApiMongoDB,
container.isPreferredApiDocumentDB,
container.isPreferredApiCassandra,
container.isPreferredApiGraph,
container.deleteCollectionText, container.deleteCollectionText,
container.deleteDatabaseText, container.deleteDatabaseText,
container.addCollectionText, container.addCollectionText,

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { AuthType } from "../../../AuthType"; import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -17,7 +18,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -56,7 +56,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -119,7 +118,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -208,15 +206,26 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
beforeEach(() => { beforeEach(() => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false);
}); });
it("Cassandra Api not available - button should be hidden", () => { it("Cassandra Api not available - button should be hidden", () => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined(); expect(openCassandraShellBtn).toBeUndefined();
@@ -281,7 +290,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
@@ -337,7 +345,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
@@ -347,6 +354,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("should only show New SQL Query and Open Query buttons", () => { it("should only show New SQL Query and Open Query buttons", () => {
updateUserContext({
databaseAccount: {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
expect(buttons.length).toBe(2); expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query"); expect(buttons[0].commandButtonLabel).toBe("New SQL Query");

View File

@@ -74,7 +74,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenMongoTerminalButton(container)); buttons.push(createOpenMongoTerminalButton(container));
} }
if (container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container)); buttons.push(createOpenCassandraTerminalButton(container));
} }
} }
@@ -90,15 +90,15 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createDivider()); buttons.push(createDivider());
} }
const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSqlQuerySupported) { if (isSqlQuerySupported) {
const newSqlQueryBtn = createNewSQLQueryButton(container); const newSqlQueryBtn = createNewSQLQueryButton(container);
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }
const isSupportedOpenQueryApi = const isSupportedOpenQueryApi =
container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph(); userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin";
const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) { if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container); const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
@@ -107,7 +107,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenQueryFromDiskButton(container)); buttons.push(createOpenQueryFromDiskButton(container));
} }
if (areScriptsSupported(container)) { if (areScriptsSupported()) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
@@ -154,25 +154,18 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
} }
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [
if (configContext.platform === Platform.Hosted) { {
return buttons;
}
if (!container.isPreferredApiCassandra()) {
const label = "Settings";
const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: label, iconAlt: "Settings",
onCommandClick: () => container.openSettingPane(), onCommandClick: () => container.openSettingPane(),
commandButtonLabel: undefined, commandButtonLabel: undefined,
ariaLabel: label, ariaLabel: "Settings",
tooltipText: label, tooltipText: "Settings",
hasPopup: true, hasPopup: true,
disabled: false, disabled: false,
}; },
buttons.push(settingsPaneButton); ];
}
if (container.isHostedDataExplorerEnabled()) { if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen"; const label = "Open Full Screen";
@@ -223,8 +216,8 @@ export function createDivider(): CommandButtonComponentProps {
}; };
} }
function areScriptsSupported(container: Explorer): boolean { function areScriptsSupported(): boolean {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
} }
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps { function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
@@ -286,7 +279,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
container.openAddDatabasePane(); container.addDatabasePane.open();
// container.openAddDatabasePane();
document.getElementById("linkAddDatabase").focus(); document.getElementById("linkAddDatabase").focus();
}, },
commandButtonLabel: label, commandButtonLabel: label,
@@ -296,7 +290,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
} }
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps { function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) { if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query"; const label = "New SQL Query";
return { return {
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
@@ -310,7 +304,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
hasPopup: true, hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(), disabled: container.isDatabaseNodeOrNoneSelected(),
}; };
} else if (container.isPreferredApiMongoDB()) { } else if (userContext.apiType === "Mongo") {
const label = "New Query"; const label = "New Query";
return { return {
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
@@ -332,8 +326,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] { export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean = const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
!container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(container);
if (shouldEnableScriptsCommands) { if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";

View File

@@ -1,9 +1,9 @@
import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react";
import { import {
NotificationConsoleComponentProps,
NotificationConsoleComponent,
ConsoleDataType, ConsoleDataType,
NotificationConsoleComponent,
NotificationConsoleComponentProps,
} from "./NotificationConsoleComponent"; } from "./NotificationConsoleComponent";
describe("NotificationConsoleComponent", () => { describe("NotificationConsoleComponent", () => {
@@ -12,7 +12,7 @@ describe("NotificationConsoleComponent", () => {
consoleData: undefined, consoleData: undefined,
isConsoleExpanded: false, isConsoleExpanded: false,
inProgressConsoleDataIdToBeDeleted: "", inProgressConsoleDataIdToBeDeleted: "",
setIsConsoleExpanded: (isExpanded: boolean): void => {}, setIsConsoleExpanded: (): void => undefined,
}; };
}; };
@@ -98,7 +98,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props); wrapper.setProps(props);
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date); expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message); expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message);
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)); expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)).toBe(true);
}; };
it("renders progress notifications", () => { it("renders progress notifications", () => {
@@ -139,7 +139,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props); wrapper.setProps(props);
wrapper.find(".clearNotificationsButton").simulate("click"); wrapper.find(".clearNotificationsButton").simulate("click");
expect(!wrapper.exists(".notificationConsoleData")); expect(wrapper.exists(".notificationConsoleData")).toBe(true);
}); });
it("collapses and hide content", () => { it("collapses and hide content", () => {
@@ -155,7 +155,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props); wrapper.setProps(props);
wrapper.find(".notificationConsoleHeader").simulate("click"); wrapper.find(".notificationConsoleHeader").simulate("click");
expect(!wrapper.exists(".notificationConsoleContent")); expect(wrapper.exists(".notificationConsoleContent")).toBe(false);
}); });
it("display latest data in header", () => { it("display latest data in header", () => {

View File

@@ -2,19 +2,20 @@
* React component for control bar * React component for control bar
*/ */
import * as React from "react";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import AnimateHeight from "react-animate-height";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react"; import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import LoadingIcon from "../../../../images/loading.svg"; import * as React from "react";
import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
import InfoIcon from "../../../../images/info_color.svg"; import InfoIcon from "../../../../images/info_color.svg";
import ErrorRedIcon from "../../../../images/error_red.svg"; import LoadingIcon from "../../../../images/loading.svg";
import ClearIcon from "../../../../images/Clear.svg";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
/** /**
* Log levels * Log levels
@@ -76,7 +77,7 @@ export class NotificationConsoleComponent extends React.Component<
public componentDidUpdate( public componentDidUpdate(
prevProps: NotificationConsoleComponentProps, prevProps: NotificationConsoleComponentProps,
prevState: NotificationConsoleComponentState prevState: NotificationConsoleComponentState
) { ): void {
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData); const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData);
if ( if (
@@ -97,7 +98,7 @@ export class NotificationConsoleComponent extends React.Component<
} }
} }
public setElememntRef = (element: HTMLElement) => { public setElememntRef = (element: HTMLElement): void => {
this.consoleHeaderElement = element; this.consoleHeaderElement = element;
}; };
@@ -116,7 +117,7 @@ export class NotificationConsoleComponent extends React.Component<
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
ref={this.setElememntRef} ref={this.setElememntRef}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()} onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0} tabIndex={0}
> >
@@ -135,6 +136,7 @@ export class NotificationConsoleComponent extends React.Component<
<span className="numInfoItems">{numInfoItems}</span> <span className="numInfoItems">{numInfoItems}</span>
</span> </span>
</span> </span>
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
<span className="consoleSplitter" /> <span className="consoleSplitter" />
<span className="headerStatus"> <span className="headerStatus">
<span className="headerStatusEllipsis">{this.state.headerStatus}</span> <span className="headerStatusEllipsis">{this.state.headerStatus}</span>
@@ -304,3 +306,18 @@ export class NotificationConsoleComponent extends React.Component<
); );
}; };
} }
const PrPreview = (props: { pr: string }) => {
const url = new URL(props.pr);
const [, ref] = url.hash.split("#");
url.hash = "";
return (
<>
<span className="consoleSplitter" />
<a target="_blank" rel="noreferrer" href={url.href} style={{ marginRight: "1em", fontWeight: "bold" }}>
{ref}
</a>
</>
);
};

View File

@@ -1,15 +1,16 @@
// Manages all the redux logic for the notebook nteract code // Manages all the redux logic for the notebook nteract code
// TODO: Merge with NotebookClient? // TODO: Merge with NotebookClient?
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import * as Constants from "../../Common/Constants";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
// Vendor modules // Vendor modules
import { import {
actions, actions,
AppState, AppState,
ContentRecord,
createHostRef, createHostRef,
createKernelspecsRef, createKernelspecsRef,
HostRecord,
HostRef,
IContentProvider,
KernelspecsRef,
makeAppRecord, makeAppRecord,
makeCommsRecord, makeCommsRecord,
makeContentsRecord, makeContentsRecord,
@@ -19,23 +20,22 @@ import {
makeJupyterHostRecord, makeJupyterHostRecord,
makeStateRecord, makeStateRecord,
makeTransformsRecord, makeTransformsRecord,
ContentRecord,
HostRecord,
HostRef,
KernelspecsRef,
IContentProvider,
} from "@nteract/core"; } from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs"; import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom"; import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable"; import * as Immutable from "immutable";
import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux";
import configureStore from "./NotebookComponent/store";
import { Notification } from "react-notification-system"; import { Notification } from "react-notification-system";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { AnyAction, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import * as Constants from "../../Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
import SandboxJavaScript from "./NotebookRenderer/outputs/SandboxJavaScript";
import SanitizedHTML from "./NotebookRenderer/outputs/SanitizedHTML";
export type KernelSpecsDisplay = { name: string; displayName: string }; export type KernelSpecsDisplay = { name: string; displayName: string };
@@ -168,8 +168,10 @@ export class NotebookClientV2 {
"application/vnd.vega.v5+json": NullTransform, "application/vnd.vega.v5+json": NullTransform,
"application/vdom.v1+json": TransformVDOM, "application/vdom.v1+json": TransformVDOM,
"application/json": Media.Json, "application/json": Media.Json,
"application/javascript": Media.JavaScript, "application/javascript": userContext.features.sandboxNotebookOutputs
"text/html": Media.HTML, ? SandboxJavaScript
: Media.JavaScript,
"text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : Media.HTML,
"text/markdown": Media.Markdown, "text/markdown": Media.Markdown,
"text/latex": Media.LaTeX, "text/latex": Media.LaTeX,
"image/svg+xml": Media.SVG, "image/svg+xml": Media.SVG,

View File

@@ -1,18 +1,20 @@
import * as React from "react";
import "./base.css";
import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core"; import { actions, ContentRef } from "@nteract/core";
import loadTransform from "../NotebookComponent/loadTransform"; import { KernelOutputError, StreamText } from "@nteract/outputs";
import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import "./default.css";
import "./NotebookReadOnlyRenderer.less"; import "./NotebookReadOnlyRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: any;
@@ -60,6 +62,16 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<CodeCell id={id} contentRef={contentRef}> <CodeCell id={id} contentRef={contentRef}>
{{ {{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef), prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => (
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined,
editor: { editor: {
monaco: (props: PassedEditorProps) => monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />, this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,

View File

@@ -1,37 +1,33 @@
import * as React from "react"; import { CellId } from "@nteract/commutable";
import "./base.css"; import { CellType } from "@nteract/commutable/src";
import "./default.css"; import { actions, ContentRef } from "@nteract/core";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components"; import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import Prompt from "./Prompt"; import * as React from "react";
import { promptContent } from "./PromptContent";
import { AzureTheme } from "./AzureTheme";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend"; import HTML5Backend from "react-dnd-html5-backend";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core"; import { userContext } from "../../../UserContext";
import { CellId } from "@nteract/commutable";
import loadTransform from "../NotebookComponent/loadTransform";
import DraggableCell from "./decorators/draggable";
import CellCreator from "./decorators/CellCreator";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import CellToolbar from "./Toolbar";
import StatusBar from "./StatusBar";
import HijackScroll from "./decorators/hijack-scroll";
import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
import CellLabeler from "./decorators/CellLabeler";
import HoverableCell from "./decorators/HoverableCell";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import "./default.css";
import MarkdownCell from "./markdown-cell";
import "./NotebookRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs";
import Prompt from "./Prompt";
import { promptContent } from "./PromptContent";
import StatusBar from "./StatusBar";
import CellToolbar from "./Toolbar";
export interface NotebookRendererBaseProps { export interface NotebookRendererBaseProps {
contentRef: any; contentRef: any;
@@ -112,6 +108,16 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
</Prompt> </Prompt>
), ),
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />, toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => (
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined,
}} }}
</CodeCell> </CodeCell>
), ),

View File

@@ -0,0 +1,160 @@
// TODO The purpose of importing this source file https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/cells/markdown-cell.tsx
// into our source is to be able to overwrite the version of react-markdown which has this fix ("escape html to false")
// https://github.com/nteract/markdown/commit/e19c7cc590a4379fc507f67a7b4228363b9d8631 without having to upgrade
// @nteract/stateful-component which causes runtime issues.
import { ImmutableCell } from "@nteract/commutable/src";
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import { MarkdownPreviewer } from "@nteract/markdown";
import { defineConfigOption } from "@nteract/mythic-configuration";
import { Source as BareSource } from "@nteract/presentational-components";
import Editor, { EditorSlots } from "@nteract/stateful-components/lib/inputs/editor";
import React from "react";
import { ReactMarkdownProps } from "react-markdown";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
const { selector: markdownConfig } = defineConfigOption({
key: "markdownOptions",
label: "Markdown Editor Options",
defaultValue: {},
});
interface NamedMDCellSlots {
editor?: EditorSlots;
toolbar?: () => JSX.Element;
}
interface ComponentProps {
id: string;
contentRef: ContentRef;
cell_type?: "markdown";
children?: NamedMDCellSlots;
}
interface StateProps {
isCellFocused: boolean;
isEditorFocused: boolean;
cell?: ImmutableCell;
markdownOptions: ReactMarkdownProps;
}
interface DispatchProps {
focusAboveCell: () => void;
focusBelowCell: () => void;
focusEditor: () => void;
unfocusEditor: () => void;
}
// Add missing style to make the editor show https://github.com/nteract/nteract/commit/7fa580011578350e56deac81359f6294fdfcad20#diff-07829a1908e4bf98d4420f868a1c6f890b95d77297b9805c9590d2dba11e80ce
export const Source = styled(BareSource)`
width: 100%;
width: -webkit-fill-available;
width: -moz-available;
`;
export class PureMarkdownCell extends React.Component<ComponentProps & DispatchProps & StateProps> {
render() {
const { contentRef, id, cell, children } = this.props;
const { isEditorFocused, isCellFocused, markdownOptions } = this.props;
const { focusAboveCell, focusBelowCell, focusEditor, unfocusEditor } = this.props;
/**
* We don't set the editor slots as defaults to support dynamic imports
* Users can continue to add the editorSlots as children
*/
const editor = children?.editor;
const toolbar = children?.toolbar;
const source = cell ? cell.get("source", "") : "";
return (
<div className="nteract-md-cell nteract-cell">
<div className="nteract-cell-row">
<div className="nteract-cell-gutter">{toolbar && toolbar()}</div>
<div className="nteract-cell-body">
<MarkdownPreviewer
focusAbove={focusAboveCell}
focusBelow={focusBelowCell}
focusEditor={focusEditor}
cellFocused={isCellFocused}
editorFocused={isEditorFocused}
unfocusEditor={unfocusEditor}
source={source}
markdownOptions={markdownOptions}
>
<Source className="nteract-cell-source">
<Editor id={id} contentRef={contentRef}>
{editor}
</Editor>
</Source>
</MarkdownPreviewer>
</div>
</div>
</div>
);
}
}
export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const { id, contentRef } = ownProps;
const mapStateToProps = (state: AppState): StateProps => {
const model = selectors.model(state, { contentRef });
let isCellFocused = false;
let isEditorFocused = false;
let cell;
if (model && model.type === "notebook") {
cell = selectors.notebook.cellById(model, { id });
isCellFocused = model.cellFocused === id;
isEditorFocused = model.editorFocused === id;
}
const markdownOptionsDefaults = {
linkTarget: "_blank",
};
const currentMarkdownOptions = markdownConfig(state);
const markdownOptions = Object.assign({}, markdownOptionsDefaults, currentMarkdownOptions);
return {
cell,
isCellFocused,
isEditorFocused,
markdownOptions,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = (
initialDispatch: Dispatch,
ownProps: ComponentProps
): ((dispatch: Dispatch) => DispatchProps) => {
const { id, contentRef } = ownProps;
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
focusAboveCell: () => {
dispatch(actions.focusPreviousCell({ id, contentRef }));
dispatch(actions.focusPreviousCellEditor({ id, contentRef }));
},
focusBelowCell: () => {
dispatch(actions.focusNextCell({ id, createCellIfUndefined: true, contentRef }));
dispatch(actions.focusNextCellEditor({ id, contentRef }));
},
focusEditor: () => dispatch(actions.focusCellEditor({ id, contentRef })),
unfocusEditor: () => dispatch(actions.focusCellEditor({ id: undefined, contentRef })),
});
return mapDispatchToProps;
};
const MarkdownCell = connect(makeMapStateToProps, makeMapDispatchToProps)(PureMarkdownCell);
export default MarkdownCell;

View File

@@ -0,0 +1,70 @@
import { AppState, ContentRef, selectors } from "@nteract/core";
import { Output } from "@nteract/outputs";
import Immutable from "immutable";
import React from "react";
import { connect } from "react-redux";
import { SandboxFrame } from "./SandboxFrame";
// Adapted from https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/outputs/index.tsx
// to add support for sandboxing using <iframe>
interface ComponentProps {
id: string;
contentRef: ContentRef;
children: React.ReactNode;
}
interface StateProps {
hidden: boolean;
expanded: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outputs: Immutable.List<any>;
}
export class IFrameOutputs extends React.PureComponent<ComponentProps & StateProps> {
render(): JSX.Element {
const { outputs, children, hidden, expanded } = this.props;
return (
<SandboxFrame
style={{ border: "none", width: "100%" }}
sandbox="allow-downloads allow-forms allow-pointer-lock allow-same-origin allow-scripts"
>
<div className={`nteract-cell-outputs ${hidden ? "hidden" : ""} ${expanded ? "expanded" : ""}`}>
{outputs.map((output, index) => (
<Output output={output} key={index}>
{children}
</Output>
))}
</div>
</SandboxFrame>
);
}
}
export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: AppState): StateProps => {
let outputs = Immutable.List();
let hidden = false;
let expanded = false;
const { contentRef, id } = ownProps;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
hidden = cell.cell_type === "code" && cell.getIn(["metadata", "jupyter", "outputs_hidden"]);
expanded = cell.cell_type === "code" && cell.getIn(["metadata", "collapsed"]) === false;
}
}
return { outputs, hidden, expanded };
};
return mapStateToProps;
};
export default connect<StateProps, void, ComponentProps, AppState>(makeMapStateToProps)(IFrameOutputs);

View File

@@ -0,0 +1,69 @@
import React from "react";
import ReactDOM from "react-dom";
import { copyStyles } from "../../../../Utils/StyleUtils";
interface SandboxFrameProps {
style: React.CSSProperties;
sandbox: string;
}
interface SandboxFrameState {
frame: HTMLIFrameElement;
frameBody: HTMLElement;
frameHeight: number;
}
export class SandboxFrame extends React.PureComponent<SandboxFrameProps, SandboxFrameState> {
private resizeObserver: ResizeObserver;
private mutationObserver: MutationObserver;
constructor(props: SandboxFrameProps) {
super(props);
this.state = {
frame: undefined,
frameBody: undefined,
frameHeight: 0,
};
}
render(): JSX.Element {
return (
<iframe
ref={(ele) => this.setState({ frame: ele })}
srcDoc={`<!DOCTYPE html>`}
onLoad={(event) => this.onFrameLoad(event)}
style={this.props.style}
sandbox={this.props.sandbox}
height={this.state.frameHeight}
>
{this.state.frameBody && ReactDOM.createPortal(this.props.children, this.state.frameBody)}
</iframe>
);
}
componentWillUnmount(): void {
this.resizeObserver?.disconnect();
this.mutationObserver?.disconnect();
}
onFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
const doc = (event.target as HTMLIFrameElement).contentDocument;
copyStyles(document, doc);
this.setState({ frameBody: doc.body });
this.mutationObserver = new MutationObserver(() => {
const bodyFirstElementChild = this.state.frameBody?.firstElementChild;
if (!this.resizeObserver && bodyFirstElementChild) {
this.resizeObserver = new ResizeObserver(() =>
this.setState({
frameHeight: this.state.frameBody?.firstElementChild.scrollHeight,
})
);
this.resizeObserver.observe(bodyFirstElementChild);
}
});
this.mutationObserver.observe(doc.body, { childList: true });
}
}

View File

@@ -0,0 +1,26 @@
import { Media } from "@nteract/outputs";
import React from "react";
interface Props {
/**
* The JavaScript code that we would like to execute.
*/
data: string;
/**
* The media type associated with our component.
*/
mediaType: "text/javascript";
}
export class SandboxJavaScript extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "application/javascript",
};
render(): JSX.Element {
return <Media.HTML data={`<script>${this.props.data}</script>`} />;
}
}
export default SandboxJavaScript;

View File

@@ -0,0 +1,38 @@
import { Media } from "@nteract/outputs";
import React from "react";
import sanitizeHtml from "sanitize-html";
interface Props {
/**
* The HTML string that will be rendered.
*/
data: string;
/**
* The media type associated with the HTML
* string. This defaults to text/html.
*/
mediaType: "text/html";
}
export class SanitizedHTML extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "text/html",
};
render(): JSX.Element {
return <Media.HTML data={sanitize(this.props.data)} />;
}
}
function sanitize(html: string): string {
return sanitizeHtml(html, {
allowedTags: false, // allow all tags
allowedAttributes: false, // allow all attrs
transformTags: {
iframe: "iframe-disabled", // disable iframes
},
});
}
export default SanitizedHTML;

View File

@@ -1,7 +1,8 @@
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import AddCollectionPane from "./AddCollectionPane";
import Explorer from "../Explorer";
import { DatabaseAccount } from "../../Contracts/DataModels"; import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import AddCollectionPane from "./AddCollectionPane";
describe("Add Collection Pane", () => { describe("Add Collection Pane", () => {
describe("isValid()", () => { describe("isValid()", () => {
@@ -50,7 +51,14 @@ describe("Add Collection Pane", () => {
}); });
it("should be false if graph API and partition key is /id or /label", () => { it("should be false if graph API and partition key is /id or /label", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase()); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id"); addCollectionPane.partitionKey("/id");
expect(addCollectionPane.isValid()).toBe(false); expect(addCollectionPane.isValid()).toBe(false);
@@ -60,7 +68,13 @@ describe("Add Collection Pane", () => {
}); });
it("should be true for any non-graph API with /id or /label partition key", () => { it("should be true for any non-graph API with /id or /label partition key", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase()); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane; const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.partitionKey("/id"); addCollectionPane.partitionKey("/id");

View File

@@ -127,13 +127,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.partitionKey.extend({ rateLimit: 100 }); this.partitionKey.extend({ rateLimit: 100 });
this.partitionKeyPattern = ko.pureComputed(() => { this.partitionKeyPattern = ko.pureComputed(() => {
if (this.container && this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
return "^/[^/]*"; return "^/[^/]*";
} }
return ".*"; return ".*";
}); });
this.partitionKeyTitle = ko.pureComputed(() => { this.partitionKeyTitle = ko.pureComputed(() => {
if (this.container && this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
return "May not use composite partition key"; return "May not use composite partition key";
} }
return ""; return "";
@@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (currentCollections >= maxCollections) { if (currentCollections >= maxCollections) {
let typeOfContainer = "collection"; let typeOfContainer = "collection";
if (this.container.isPreferredApiGraph() || this.container.isPreferredApiTable()) { if (userContext.apiType === "Gremlin" || this.container.isPreferredApiTable()) {
typeOfContainer = "container"; typeOfContainer = "container";
} }
@@ -368,7 +368,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return "e.g., address.zipCode"; return "e.g., address.zipCode";
} }
if (this.container && !!this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
return "e.g., /address"; return "e.g., /address";
} }
@@ -384,17 +384,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.uniqueKeysVisible = ko.pureComputed<boolean>(() => { this.uniqueKeysVisible = ko.pureComputed<boolean>(() => {
if ( if (userContext.apiType === "SQL") {
this.container == null || return true;
!!this.container.isPreferredApiMongoDB() ||
!!this.container.isPreferredApiTable() ||
!!this.container.isPreferredApiCassandra() ||
!!this.container.isPreferredApiGraph()
) {
return false;
} }
return true; return false;
}); });
this.partitionKeyVisible = ko.computed<boolean>(() => { this.partitionKeyVisible = ko.computed<boolean>(() => {
@@ -591,7 +585,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false; return false;
} }
if (this.container.isPreferredApiDocumentDB()) { if (userContext.apiType === "SQL") {
return true; return true;
} }
@@ -599,7 +593,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true; return true;
} }
if (this.container.isPreferredApiCassandra() && this.container.hasStorageAnalyticsAfecFeature()) { if (userContext.apiType === "Cassandra" && this.container.hasStorageAnalyticsAfecFeature()) {
return true; return true;
} }
@@ -1011,7 +1005,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false; return false;
} }
if (this.container.isPreferredApiGraph() && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) { if (userContext.apiType === "Gremlin" && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) {
this.formErrors("/id and /label as partition keys are not allowed for graph."); this.formErrors("/id and /label as partition keys are not allowed for graph.");
return false; return false;
} }

View File

@@ -0,0 +1,460 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import * as SharedConstants from "../../Shared/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
public databaseIdLabel: ko.Computed<string>;
public databaseIdPlaceHolder: ko.Computed<string>;
public databaseId: ko.Observable<string>;
public databaseIdTooltipText: ko.Computed<string>;
public databaseLevelThroughputTooltipText: ko.Computed<string>;
public databaseCreateNewShared: ko.Observable<boolean>;
public formErrorsDetails: ko.Observable<string>;
public throughput: ViewModels.Editable<number>;
public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>;
public maxThroughputRUText: ko.PureComputed<string>;
public throughputRangeText: ko.Computed<string>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public requestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public costsVisible: ko.PureComputed<boolean>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
public upsellAnchorUrl: ko.PureComputed<string>;
public upsellAnchorText: ko.PureComputed<string>;
public isAutoPilotSelected: ko.Observable<boolean>;
public maxAutoPilotThroughputSet: ko.Observable<number>;
public autoPilotUsageCost: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>;
public freeTierExceedThroughputTooltip: ko.Computed<string>;
public isFreeTierAccount: ko.Computed<boolean>;
public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title((this.container && this.container.addDatabaseText()) || "New Database");
this.databaseId = ko.observable<string>();
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
// TODO 388844: get defaults from parent frame
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.databaseIdLabel = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Keyspace id" : "Database id"
);
this.databaseIdPlaceHolder = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Type a new keyspace id" : "Type a new database id"
);
this.databaseIdTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"
}`;
});
this.databaseLevelThroughputTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
});
this.throughput = editable.observable<number>();
this.maxThroughputRU = ko.observable<number>();
this.minThroughputRU = ko.observable<number>();
this.throughputSpendAckText = ko.observable<string>();
this.throughputSpendAck = ko.observable<boolean>(false);
this.isAutoPilotSelected = ko.observable<boolean>(false);
this.maxAutoPilotThroughputSet = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (!autoPilot) {
return "";
}
return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */);
});
this.throughputRangeText = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
this.requestUnitsUsageCost = ko.computed(() => {
const offerThroughput: number = this.throughput();
if (
offerThroughput < this.minThroughputRU() ||
(offerThroughput > this.maxThroughputRU() && !this.canExceedMaximumValue())
) {
return "";
}
const account = this.container.databaseAccount();
if (!account) {
return "";
}
const regions =
(account &&
account.properties &&
account.properties.readLocations &&
account.properties.readLocations.length) ||
1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
let estimatedSpendAcknowledge: string;
let estimatedSpend: string;
if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput,
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.maxAutoPilotThroughputSet(),
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.maxAutoPilotThroughputSet(),
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
}
// TODO: change throughputSpendAckText to be a computed value, instead of having this side effect
this.throughputSpendAckText(estimatedSpendAcknowledge);
return estimatedSpend;
});
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
const isFreeTierAccount =
databaseAccount && databaseAccount.properties && databaseAccount.properties.enableFreeTier;
return isFreeTierAccount;
});
this.showUpsellMessage = ko.pureComputed(() => {
if (this.container.isServerlessEnabled()) {
return false;
}
if (this.isFreeTierAccount()) {
return this.databaseCreateNewShared();
}
return true;
});
this.maxThroughputRUText = ko.pureComputed(() => {
return this.maxThroughputRU().toLocaleString();
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
const selectedThroughput: number = this.throughput();
const maxRU: number = this.maxThroughputRU && this.maxThroughputRU();
const isMaxRUGreaterThanDefault: boolean = maxRU > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
const isThroughputSetGreaterThanDefault: boolean =
selectedThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
if (this.canExceedMaximumValue()) {
return isThroughputSetGreaterThanDefault;
}
return isThroughputSetGreaterThanDefault && isMaxRUGreaterThanDefault;
});
this.databaseCreateNewShared.subscribe((useShared: boolean) => {
this._updateThroughputLimitByDatabase();
});
this.resetData();
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: ""
);
this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage(
userContext.portalEnv,
this.isFreeTierAccount(),
this.container.isFirstResourceCreated(),
this.container.defaultExperience(),
false
);
});
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
return `${this.upsellMessage()}. Click ${this.isFreeTierAccount() ? "to learn more" : "for more details"}`;
});
this.upsellAnchorUrl = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
});
this.upsellAnchorText = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? "Learn more" : "More details";
});
}
public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.showErrorDetails();
return false;
}
return true;
};
public open() {
super.open();
this.resetData();
const addDatabasePaneOpenMessage = {
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const focusElement = document.getElementById("database-id");
focusElement && focusElement.focus();
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
}
public submit() {
if (!this._isValid()) {
return;
}
const offerThroughput: number = this._computeOfferThroughput();
const addDatabasePaneStartMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
this.formErrors("");
this.isExecuting(true);
const createDatabaseParams: DataModels.CreateDatabaseParams = {
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
};
if (this.isAutoPilotSelected()) {
createDatabaseParams.autoPilotMaxThroughput = this.maxAutoPilotThroughputSet();
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
}
createDatabase(createDatabaseParams).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, startKey);
},
(error: any) => {
this._onCreateDatabaseFailure(error, offerThroughput, startKey);
}
);
}
public resetData() {
this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput);
this._updateThroughputLimitByDatabase();
this.throughputSpendAck(false);
super.resetData();
}
public getSharedThroughputDefault(): boolean {
const subscriptionType = userContext.subscriptionType;
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
return true;
}
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
const addDatabasePaneSuccessMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey);
this.resetData();
}
private _onCreateDatabaseFailure(error: any, offerThroughput: number, startKey: number): void {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
const addDatabasePaneFailedMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
}
private _getThroughput(): number {
const throughput: number = this.throughput();
return isNaN(throughput) ? 0 : Number(throughput);
}
private _computeOfferThroughput(): number {
if (!this.canConfigureThroughput()) {
return undefined;
}
if (this.isAutoPilotSelected()) {
return undefined;
}
return this._getThroughput();
}
private _isValid(): boolean {
// TODO add feature flag that disables validation for customers with custom accounts
if (this.isAutoPilotSelected()) {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
this.formErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
}
const throughput = this._getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (
this.isAutoPilotSelected() &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
return true;
}
private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings {
if (this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) {
return {
maxThroughput: this.maxAutoPilotThroughputSet() * 1,
};
}
return undefined;
}
private _updateThroughputLimitByDatabase() {
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.throughput(throughputDefaults.shared);
this.maxThroughputRU(throughputDefaults.unlimitedmax);
this.minThroughputRU(throughputDefaults.unlimitedmin);
}
}

View File

@@ -1,7 +1,7 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { AddDatabasePane } from ".";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { AddDatabasePane } from "../AddDatabasePane";
const props = { const props = {
explorer: new Explorer(), explorer: new Explorer(),
closePanel: (): void => undefined, closePanel: (): void => undefined,

View File

@@ -1,108 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="deletecollectionconfirmationpane">
<!-- Delete Collection Confirmation form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Delete Collection Confirmation header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Delete Collection Confirmation header - End -->
<div class="warningErrorContainer" data-bind="visible: !formErrors()">
<div class="warningErrorContent">
<span><img class="paneWarningIcon" src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer">
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this
resource and all of its children resources.
</span>
</div>
</div>
<!-- Delete Collection Confirmation errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="
visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a class="errorLink" role="link" data-bind="click: showErrorDetails">More details</a>
</span>
</div>
</div>
<!-- Delete Collection Confirmation errors - End -->
<!-- Delete Collection Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<span class="mandatoryStar">*</span> <span data-bind="text: collectionIdConfirmationText"></span>
<p>
<input
type="text"
data-test="confirmCollectionId"
name="collectionIdConfirmation"
required
class="collid"
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }"
/>
</p>
</div>
<div data-bind="visible: recordDeleteFeedback">
<div>Help us improve Azure Cosmos DB!</div>
<div>What is the reason why you are deleting this container?</div>
<p>
<textarea
type="text"
data-test="containerDeleteFeedback"
name="containerDeleteFeedback"
rows="3"
cols="53"
class="collid"
maxlength="512"
data-bind="value: containerDeleteFeedback"
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this container?"
>
</textarea>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="deleteCollection" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Delete Collection Confirmation inputs - End -->
</form>
</div>
<!-- Delete Collection Confirmation form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,128 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import DeleteFeedback from "../../Common/DeleteFeedback";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteCollectionConfirmationPane extends ContextualPaneBase {
public collectionIdConfirmationText: ko.Observable<string>;
public collectionIdConfirmation: ko.Observable<string>;
public containerDeleteFeedback: ko.Observable<string>;
public recordDeleteFeedback: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.collectionIdConfirmationText = ko.observable<string>("Confirm by typing the collection id");
this.collectionIdConfirmation = ko.observable<string>();
this.containerDeleteFeedback = ko.observable<string>();
this.recordDeleteFeedback = ko.observable<boolean>(false);
this.title("Delete Collection");
this.resetData();
}
public submit(): Promise<any> {
if (!this._isValid()) {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
this.formErrors("Input collection name does not match the selected collection");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting collection ${selectedCollection && selectedCollection.id()}: ${this.formErrors()}`
);
return Promise.resolve();
}
this.formErrors("");
this.isExecuting(true);
const selectedCollection = <ViewModels.Collection>this.container.findSelectedCollection();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
});
return deleteCollection(selectedCollection.databaseId, selectedCollection.id()).then(
() => {
this.isExecuting(false);
this.close();
this.container.selectedNode(selectedCollection.database);
this.container.tabsManager?.closeTabsByComparator(
(tab) =>
tab.node?.id() === selectedCollection.id() &&
(tab.node as ViewModels.Collection).databaseId === selectedCollection.databaseId
);
this.container.refreshAllDatabases();
this.resetData();
TelemetryProcessor.traceSuccess(
Action.DeleteCollection,
{
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
},
startKey
);
if (this.shouldRecordFeedback()) {
let deleteFeedback = new DeleteFeedback(
this.container.databaseAccount().id,
this.container.databaseAccount().name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
this.containerDeleteFeedback()
);
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
this.containerDeleteFeedback("");
}
},
(error: any) => {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
);
}
public resetData() {
this.collectionIdConfirmation("");
super.resetData();
}
public open() {
this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open();
}
public shouldRecordFeedback(): boolean {
return this.container.isLastCollection() && !this.container.isSelectedDatabaseShared();
}
private _isValid(): boolean {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
if (!selectedCollection) {
return false;
}
return this.collectionIdConfirmation() === selectedCollection.id();
}
}

View File

@@ -1,178 +0,0 @@
import { Text, TextField } from "office-ui-fabric-react";
import * as React from "react";
import { Areas } from "../../Common/Constants";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection } from "../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
export interface DeleteCollectionConfirmationPanelProps {
explorer: Explorer;
closePanel: () => void;
openNotificationConsole: () => void;
}
export interface DeleteCollectionConfirmationPanelState {
formError: string;
isExecuting: boolean;
}
export class DeleteCollectionConfirmationPanel extends React.Component<
DeleteCollectionConfirmationPanelProps,
DeleteCollectionConfirmationPanelState
> {
private inputCollectionName: string;
private deleteCollectionFeedback: string;
constructor(props: DeleteCollectionConfirmationPanelProps) {
super(props);
this.state = {
formError: "",
isExecuting: false,
};
}
render(): JSX.Element {
return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
<PanelInfoErrorComponent {...this.getPanelErrorProps()} />
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the collection id</Text>
<TextField
id="confirmCollectionId"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
this.inputCollectionName = newInput;
}}
/>
</div>
{this.shouldRecordFeedback() && (
<div className="deleteCollectionFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
What is the reason why you are deleting this container?
</Text>
<TextField
id="deleteCollectionFeedbackInput"
styles={{ fieldGroup: { width: 300 } }}
multiline
rows={3}
onChange={(event, newInput?: string) => {
this.deleteCollectionFeedback = newInput;
}}
/>
</div>
)}
</div>
<PanelFooterComponent buttonLabel="OK" />
{this.state.isExecuting && <PanelLoadingScreen />}
</form>
);
}
private getPanelErrorProps(): PanelInfoErrorProps {
if (this.state.formError) {
return {
messageType: "error",
message: this.state.formError,
showErrorDetails: true,
openNotificationConsole: this.props.openNotificationConsole,
};
}
return {
messageType: "warning",
showErrorDetails: false,
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
}
private shouldRecordFeedback(): boolean {
return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared();
}
public async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
const collection = this.props.explorer.findSelectedCollection();
if (!collection || this.inputCollectionName !== collection.id()) {
const errorMessage = "Input collection name does not match the selected collection";
this.setState({ formError: errorMessage });
NotificationConsoleUtils.logConsoleError(`Error while deleting collection ${collection.id()}: ${errorMessage}`);
return;
}
this.setState({ formError: "", isExecuting: true });
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Collection",
});
try {
await deleteCollection(collection.databaseId, collection.id());
this.setState({ isExecuting: false });
this.props.explorer.selectedNode(collection.database);
this.props.explorer.tabsManager?.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);
this.props.explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(
Action.DeleteCollection,
{
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Collection",
},
startKey
);
if (this.shouldRecordFeedback()) {
const deleteFeedback = new DeleteFeedback(
userContext.databaseAccount?.id,
userContext.databaseAccount?.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
this.deleteCollectionFeedback
);
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
}
this.props.closePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
this.setState({ formError: errorMessage, isExecuting: false });
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Collection",
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
}
}

View File

@@ -1,19 +1,18 @@
jest.mock("../../Common/dataAccess/deleteCollection"); jest.mock("../../../Common/dataAccess/deleteCollection");
jest.mock("../../Shared/Telemetry/TelemetryProcessor"); jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
import { Collection, Database } from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { mount, ReactWrapper, shallow } from "enzyme"; import { mount, ReactWrapper, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react"; import React from "react";
import DeleteFeedback from "../../Common/DeleteFeedback"; import { DeleteCollectionConfirmationPanel } from ".";
import Explorer from "../Explorer"; import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import DeleteFeedback from "../../../Common/DeleteFeedback";
import { TreeNode } from "../../Contracts/ViewModels"; import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
import { DeleteCollectionConfirmationPanel } from "./DeleteCollectionConfirmationPanel"; import { DefaultAccountExperienceType } from "../../../DefaultAccountExperienceType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { updateUserContext } from "../../UserContext"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
describe("Delete Collection Confirmation Pane", () => { describe("Delete Collection Confirmation Pane", () => {
describe("Explorer.isLastCollection()", () => { describe("Explorer.isLastCollection()", () => {
@@ -64,7 +63,7 @@ describe("Delete Collection Confirmation Pane", () => {
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
closePanel: (): void => undefined, closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined, collectionName: "container",
}; };
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />); const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
@@ -118,7 +117,7 @@ describe("Delete Collection Confirmation Pane", () => {
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
closePanel: (): void => undefined, closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined, collectionName: "container",
}; };
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />); wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
}); });
@@ -132,8 +131,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: selectedCollectionId } }); .simulate("change", { target: { value: selectedCollectionId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
wrapper.unmount(); wrapper.unmount();
@@ -153,8 +152,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: feedbackText } }); .simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
const deleteFeedback = new DeleteFeedback( const deleteFeedback = new DeleteFeedback(

View File

@@ -0,0 +1,152 @@
import { Text, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../../Common/Constants";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Collection } from "../../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
export interface DeleteCollectionConfirmationPanelProps {
explorer: Explorer;
collectionName: string;
closePanel: () => void;
}
export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollectionConfirmationPanelProps> = ({
explorer,
closePanel,
collectionName,
}: DeleteCollectionConfirmationPanelProps) => {
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
const [inputCollectionName, setInputCollectionName] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean => {
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
};
const paneTitle = "Delete " + collectionName;
const submit = async (): Promise<void> => {
const collection = explorer.findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
setFormError(errorMessage);
NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`
);
return;
}
const paneInfo = {
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle,
};
setFormError("");
setIsExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, paneInfo);
try {
await deleteCollection(collection.databaseId, collection.id());
setIsExecuting(false);
explorer.selectedNode(collection.database);
explorer.tabsManager?.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);
explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);
if (shouldRecordFeedback()) {
const deleteFeedback = new DeleteFeedback(
userContext.databaseAccount?.id,
userContext.databaseAccount?.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
deleteCollectionFeedback
);
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
}
closePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
setIsExecuting(false);
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
...paneInfo,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
};
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: formError,
formErrorDetail: formError,
id: "deleteCollectionpane",
isExecuting,
title: paneTitle,
submitButtonText: "OK",
onClose: closePanel,
onSubmit: submit,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField
id="confirmCollectionId"
autoFocus
value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
setInputCollectionName(newInput);
}}
/>
</div>
{shouldRecordFeedback() && (
<div className="deleteCollectionFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
What is the reason why you are deleting this {collectionName}?
</Text>
<TextField
id="deleteCollectionFeedbackInput"
styles={{ fieldGroup: { width: 300 } }}
multiline
value={deleteCollectionFeedback}
rows={3}
onChange={(event, newInput?: string) => {
setDeleteCollectionFeedback(newInput);
}}
/>
</div>
)}
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -4,6 +4,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<ExecuteSprocParamsPanel <ExecuteSprocParamsPanel
closePanel={[Function]} closePanel={[Function]}
explorer={Object {}} explorer={Object {}}
storedProcedure={Object {}}
> >
<GenericRightPaneComponent <GenericRightPaneComponent
container={Object {}} container={Object {}}
@@ -1148,7 +1149,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
isAddRemoveVisible={false} isAddRemoveVisible={false}
onParamKeyChange={[Function]} onParamKeyChange={[Function]}
onParamValueChange={[Function]} onParamValueChange={[Function]}
paramValue=""
selectedKey="string" selectedKey="string"
> >
<StyledLabelBase> <StyledLabelBase>
@@ -2683,7 +2683,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
key=".0:$.1" key=".0:$.1"
label="Value" label="Value"
onChange={[Function]} onChange={[Function]}
value=""
> >
<TextFieldBase <TextFieldBase
autoFocus={true} autoFocus={true}
@@ -2968,7 +2967,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
} }
validateOnLoad={true} validateOnLoad={true}
value=""
> >
<div <div
className="ms-TextField root-102" className="ms-TextField root-102"

View File

@@ -1,12 +1,15 @@
import { mount } from "enzyme"; import { mount } from "enzyme";
import React from "react"; import React from "react";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure";
import { ExecuteSprocParamsPanel } from "./index"; import { ExecuteSprocParamsPanel } from "./index";
describe("Excute Sproc Param Pane", () => { describe("Excute Sproc Param Pane", () => {
const fakeExplorer = {} as Explorer; const fakeExplorer = {} as Explorer;
const fakeSproc = {} as StoredProcedure;
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
storedProcedure: fakeSproc,
closePanel: (): void => undefined, closePanel: (): void => undefined,
}; };

View File

@@ -3,11 +3,13 @@ import { IDropdownOption, IImageProps, Image, Stack, Text } from "office-ui-fabr
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent"; import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
import { InputParameter } from "./InputParameter"; import { InputParameter } from "./InputParameter";
interface ExecuteSprocParamsPaneProps { interface ExecuteSprocParamsPaneProps {
explorer: Explorer; explorer: Explorer;
storedProcedure: StoredProcedure;
closePanel: () => void; closePanel: () => void;
} }
@@ -23,11 +25,12 @@ interface UnwrappedExecuteSprocParam {
export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({ export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
explorer, explorer,
storedProcedure,
closePanel, closePanel,
}: ExecuteSprocParamsPaneProps): JSX.Element => { }: ExecuteSprocParamsPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]); const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
const [partitionValue, setPartitionValue] = useState<string>(""); const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" }); const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>(""); const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
@@ -76,9 +79,15 @@ export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPanePr
return; return;
} }
setLoadingTrue(); setLoadingTrue();
const sprocParams = wrappedSprocParams && wrappedSprocParams.map((sprocParam) => sprocParam.text); const sprocParams =
const currentSelectedSproc = explorer.findSelectedStoredProcedure(); wrappedSprocParams &&
currentSelectedSproc.execute(sprocParams, partitionValue); wrappedSprocParams.map((sprocParam) => {
if (sprocParam.key === "custom") {
return JSON.parse(sprocParam.text);
}
return sprocParam.text;
});
storedProcedure.execute(sprocParams, partitionKey === "custom" ? JSON.parse(partitionValue) : partitionValue);
setLoadingFalse(); setLoadingFalse();
closePanel(); closePanel();
}; };

View File

@@ -1,142 +0,0 @@
import { Subscription } from "knockout";
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import * as React from "react";
import ErrorRedIcon from "../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../Common/Constants";
import Explorer from "../Explorer";
export interface GenericRightPaneProps {
container: Explorer;
formError: string;
formErrorDetail: string;
id: string;
isExecuting: boolean;
onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
title: string;
isSubmitButtonHidden?: boolean;
}
export interface GenericRightPaneState {
panelHeight: number;
}
export class GenericRightPaneComponent extends React.Component<GenericRightPaneProps, GenericRightPaneState> {
private notificationConsoleSubscription: Subscription;
constructor(props: GenericRightPaneProps) {
super(props);
this.state = {
panelHeight: this.getPanelHeight(),
};
}
public componentWillUnmount(): void {
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
}
public render(): JSX.Element {
return (
<div tabIndex={-1} onKeyDown={this.onKeyDown}>
<div className="contextual-pane-out" onClick={this.props.onClose}></div>
<div
className="contextual-pane"
id={this.props.id}
style={{ height: this.state.panelHeight }}
onKeyDown={this.onKeyDown}
>
<div className="panelContentWrapper">
{this.renderPanelHeader()}
{this.renderErrorSection()}
{this.props.children}
{this.renderPanelFooter()}
</div>
{this.renderLoadingScreen()}
</div>
</div>
);
}
private renderPanelHeader = (): JSX.Element => {
return (
<div className="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level={2}>
{this.props.title}
</span>
<IconButton
ariaLabel="Close pane"
title="Close pane"
onClick={this.props.onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
private renderErrorSection = (): JSX.Element => {
return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
<div className="warningErrorContent">
<span>
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
<span className="formErrors" title={this.props.formError}>
{this.props.formError}
</span>
<a className="errorLink" role="link" hidden={!this.props.formErrorDetail} onClick={this.showErrorDetail}>
More details
</a>
</span>
</div>
</div>
);
};
private renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
style={{ visibility: this.props.isSubmitButtonHidden ? "hidden" : "visible" }}
ariaLabel="Submit"
title="Submit"
onClick={this.props.onSubmit}
tabIndex={0}
className="genericPaneSubmitBtn"
text={this.props.submitButtonText}
/>
</div>
</div>
);
};
private renderLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
this.props.onClose();
event.stopPropagation();
}
};
private showErrorDetail = (): void => {
this.props.container.expandConsole();
};
private getPanelHeight = (): number => {
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
return window.innerHeight - $(notificationConsoleElement).height();
};
}

View File

@@ -0,0 +1,135 @@
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import React, { FunctionComponent, ReactNode } from "react";
import ErrorRedIcon from "../../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
export interface GenericRightPaneProps {
container: Explorer;
formError: string;
formErrorDetail: string;
id: string;
isExecuting: boolean;
onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
title: string;
isSubmitButtonHidden?: boolean;
children?: ReactNode;
}
export interface GenericRightPaneState {
panelHeight: number;
}
export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({
container,
formError,
formErrorDetail,
id,
isExecuting,
onClose,
onSubmit,
submitButtonText,
title,
isSubmitButtonHidden,
children,
}: GenericRightPaneProps) => {
const getPanelHeight = (): number => {
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
return window.innerHeight - $(notificationConsoleElement).height();
};
const panelHeight: number = getPanelHeight();
const renderPanelHeader = (): JSX.Element => {
return (
<div className="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level={2}>
{title}
</span>
<IconButton
ariaLabel="Close pane"
title="Close pane"
onClick={onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
const renderErrorSection = (): JSX.Element => {
return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!formError}>
<div className="warningErrorContent">
<span>
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
<span className="formErrors" title={formError}>
{formError}
</span>
<a className="errorLink" role="link" hidden={!formErrorDetail} onClick={showErrorDetail}>
More details
</a>
</span>
</div>
</div>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
style={{ visibility: isSubmitButtonHidden ? "hidden" : "visible" }}
ariaLabel="Submit"
title="Submit"
onClick={onSubmit}
tabIndex={0}
className="genericPaneSubmitBtn"
text={submitButtonText}
/>
</div>
</div>
);
};
const renderLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
onClose();
event.stopPropagation();
}
};
const showErrorDetail = (): void => {
container.expandConsole();
};
return (
<div tabIndex={-1} onKeyDown={onKeyDown}>
<div className="contextual-pane-out" onClick={onClose}></div>
<div className="contextual-pane" id={id} style={{ height: panelHeight }} onKeyDown={onKeyDown}>
<div className="panelContentWrapper">
{renderPanelHeader()}
{renderErrorSection()}
{children}
{renderPanelFooter()}
</div>
{renderLoadingScreen()}
</div>
</div>
);
};

View File

@@ -1,6 +1,5 @@
import AddCollectionPaneTemplate from "./AddCollectionPane.html"; import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html"; import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html"; import GitHubReposPaneTemplate from "./GitHubReposPane.html";
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html"; import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html"; import GraphStylingPaneTemplate from "./GraphStylingPane.html";
@@ -8,7 +7,6 @@ import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
import StringInputPaneTemplate from "./StringInputPane.html"; import StringInputPaneTemplate from "./StringInputPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html"; import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html"; import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
export class PaneComponent { export class PaneComponent {
constructor(data: any) { constructor(data: any) {
@@ -25,15 +23,6 @@ export class AddCollectionPaneComponent {
} }
} }
export class DeleteCollectionConfirmationPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: DeleteCollectionConfirmationPaneTemplate,
};
}
}
export class GraphNewVertexPaneComponent { export class GraphNewVertexPaneComponent {
constructor() { constructor() {
return { return {
@@ -69,16 +58,6 @@ export class TableEditEntityPaneComponent {
}; };
} }
} }
export class TableQuerySelectPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: TableQuerySelectPaneTemplate,
};
}
}
export class CassandraAddCollectionPaneComponent { export class CassandraAddCollectionPaneComponent {
constructor() { constructor() {
return { return {

View File

@@ -152,3 +152,6 @@
.removeIcon { .removeIcon {
color: @InfoIconColor; color: @InfoIconColor;
} }
.column-select-view {
margin: 20px 0px 0px 0px;
}

View File

@@ -91,21 +91,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -196,27 +181,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -507,21 +471,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -590,9 +539,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -632,27 +578,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -750,6 +675,41 @@ exports[`Settings Pane should render Default properly 1`] = `
<div <div
className="paneMainContent" className="paneMainContent"
> >
<div
className="settingsSection"
>
<div
className="settingsSectionPart pageOptionsPart"
>
<div
className="settingsSectionLabel"
>
Page options
<Tooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</Tooltip>
</div>
<StyledChoiceGroupBase
onChange={[Function]}
options={
Array [
Object {
"key": "custom",
"text": "Custom",
},
Object {
"key": "unlimited",
"text": "Unlimited",
},
]
}
selectedKey="unlimited"
/>
</div>
<div
className="tabs settingsSectionPart"
/>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >
@@ -932,21 +892,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -1037,27 +982,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -1348,21 +1272,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -1431,9 +1340,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -1473,27 +1379,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],

View File

@@ -1,10 +1,11 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient"; import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import * as Entities from "../../Tables/Entities"; import * as Entities from "../../Tables/Entities";
import * as TableConstants from "../../Tables/Constants"; import { CassandraAPIDataClient, CassandraTableKey } from "../../Tables/TableDataClient";
import * as Utilities from "../../Tables/Utilities"; import * as Utilities from "../../Tables/Utilities";
import EntityPropertyViewModel from "./EntityPropertyViewModel"; import EntityPropertyViewModel from "./EntityPropertyViewModel";
import TableEntityPane from "./TableEntityPane"; import TableEntityPane from "./TableEntityPane";
@@ -24,11 +25,9 @@ export default class AddTableEntityPane extends TableEntityPane {
constructor(options: ViewModels.PaneOptions) { constructor(options: ViewModels.PaneOptions) {
super(options); super(options);
this.submitButtonText("Add Entity"); this.submitButtonText("Add Entity");
this.container.isPreferredApiCassandra.subscribe((isCassandra) => { if (userContext.apiType === "Cassandra") {
if (isCassandra) { this.submitButtonText("Add Row");
this.submitButtonText("Add Row"); }
}
});
this.scrollId = ko.observable<string>("addEntityScroll"); this.scrollId = ko.observable<string>("addEntityScroll");
} }
@@ -57,7 +56,7 @@ export default class AddTableEntityPane extends TableEntityPane {
headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey]; headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey];
} }
} }
if (this.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
(<CassandraAPIDataClient>this.container.tableDataClient) (<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection) .getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((columns: CassandraTableKey[]) => { .then((columns: CassandraTableKey[]) => {
@@ -94,7 +93,7 @@ export default class AddTableEntityPane extends TableEntityPane {
headers && headers &&
headers.forEach((key: string) => { headers.forEach((key: string) => {
if (!_.contains<string>(AddTableEntityPane._excludedFields, key)) { if (!_.contains<string>(AddTableEntityPane._excludedFields, key)) {
if (this.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) .concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property); .map((key) => key.property);

View File

@@ -1,14 +1,15 @@
import * as ko from "knockout"; import * as ko from "knockout";
import _ from "underscore"; import _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient"; import { userContext } from "../../../UserContext";
import * as Entities from "../../Tables/Entities";
import TableEntityPane from "./TableEntityPane";
import * as Utilities from "../../Tables/Utilities";
import * as TableConstants from "../../Tables/Constants";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as TableConstants from "../../Tables/Constants";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, CassandraTableKey } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import TableEntityPane from "./TableEntityPane";
export default class EditTableEntityPane extends TableEntityPane { export default class EditTableEntityPane extends TableEntityPane {
container: Explorer; container: Explorer;
@@ -21,11 +22,9 @@ export default class EditTableEntityPane extends TableEntityPane {
constructor(options: ViewModels.PaneOptions) { constructor(options: ViewModels.PaneOptions) {
super(options); super(options);
this.submitButtonText("Update Entity"); this.submitButtonText("Update Entity");
this.container.isPreferredApiCassandra.subscribe((isCassandra) => { if (userContext.apiType === "Cassandra") {
if (isCassandra) { this.submitButtonText("Update Row");
this.submitButtonText("Update Row"); }
}
});
this.scrollId = ko.observable<string>("editEntityScroll"); this.scrollId = ko.observable<string>("editEntityScroll");
} }
@@ -44,7 +43,7 @@ export default class EditTableEntityPane extends TableEntityPane {
property !== TableEntityProcessor.keyProperties.etag && property !== TableEntityProcessor.keyProperties.etag &&
property !== TableEntityProcessor.keyProperties.resourceId && property !== TableEntityProcessor.keyProperties.resourceId &&
property !== TableEntityProcessor.keyProperties.self && property !== TableEntityProcessor.keyProperties.self &&
(!this.container.isPreferredApiCassandra() || property !== TableConstants.EntityKeyNames.RowKey) (userContext.apiType !== "Cassandra" || property !== TableConstants.EntityKeyNames.RowKey)
) { ) {
numberOfProperties++; numberOfProperties++;
} }
@@ -93,9 +92,9 @@ export default class EditTableEntityPane extends TableEntityPane {
key !== TableEntityProcessor.keyProperties.etag && key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId && key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self && key !== TableEntityProcessor.keyProperties.self &&
(!this.container.isPreferredApiCassandra() || key !== TableConstants.EntityKeyNames.RowKey) (userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
) { ) {
if (this.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys) .concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property); .map((key) => key.property);
@@ -150,7 +149,7 @@ export default class EditTableEntityPane extends TableEntityPane {
} }
} }
}); });
if (this.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
(<CassandraAPIDataClient>this.container.tableDataClient) (<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection) .getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((properties: CassandraTableKey[]) => { .then((properties: CassandraTableKey[]) => {
@@ -169,10 +168,7 @@ export default class EditTableEntityPane extends TableEntityPane {
var updatedEntity: any = {}; var updatedEntity: any = {};
displayedAttributes && displayedAttributes &&
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => { displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
if ( if (attribute.name() && (userContext.apiType !== "Cassandra" || attribute.value() !== "")) {
attribute.name() &&
(!this.tableViewModel.queryTablesTab.container.isPreferredApiCassandra() || attribute.value() !== "")
) {
var value = attribute.getPropertyValue(); var value = attribute.getPropertyValue();
var type = attribute.type(); var type = attribute.type();
if (type === TableConstants.TableType.Int64) { if (type === TableConstants.TableType.Int64) {

View File

@@ -1,174 +0,0 @@
import * as ko from "knockout";
import _ from "underscore";
import * as Constants from "../../Tables/Constants";
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ContextualPaneBase } from "../ContextualPaneBase";
export interface ISelectColumn {
columnName: ko.Observable<string>;
selected: ko.Observable<boolean>;
editable: ko.Observable<boolean>;
}
export class QuerySelectPane extends ContextualPaneBase {
public titleLabel: string = "Select Columns";
public instructionLabel: string = "Select the columns that you want to query.";
public availableColumnsTableQueryLabel: string = "Available Columns";
public noColumnSelectedWarning: string = "At least one column should be selected.";
public columnOptions: ko.ObservableArray<ISelectColumn>;
public anyColumnSelected: ko.Computed<boolean>;
public canSelectAll: ko.Computed<boolean>;
public allSelected: ko.Computed<boolean>;
private selectedColumnOption: ISelectColumn = null;
public queryViewModel: QueryViewModel;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.columnOptions = ko.observableArray<ISelectColumn>();
this.anyColumnSelected = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return value.selected();
});
});
this.canSelectAll = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return !value.selected();
});
});
this.allSelected = ko.pureComputed<boolean>({
read: () => {
return !this.canSelectAll();
},
write: (value) => {
if (value) {
this.selectAll();
} else {
this.clearAll();
}
},
owner: this,
});
}
public submit() {
this.queryViewModel.selectText(this.getParameters());
this.queryViewModel.getSelectMessage();
this.close();
}
public open() {
this.setTableColumns(this.queryViewModel.columnOptions());
this.setDisplayedColumns(this.queryViewModel.selectText(), this.columnOptions());
super.open();
}
private getParameters(): string[] {
if (this.canSelectAll() === false) {
return [];
}
var selectedColumns = this.columnOptions().filter((value: ISelectColumn) => value.selected() === true);
var columns: string[] = selectedColumns.map((value: ISelectColumn) => {
var name: string = value.columnName();
return name;
});
return columns;
}
public setTableColumns(columnNames: string[]): void {
var columns: ISelectColumn[] = columnNames.map((value: string) => {
var columnOption: ISelectColumn = {
columnName: ko.observable<string>(value),
selected: ko.observable<boolean>(true),
editable: ko.observable<boolean>(this.isEntityEditable(value)),
};
return columnOption;
});
this.columnOptions(columns);
}
public setDisplayedColumns(querySelect: string[], columns: ISelectColumn[]): void {
if (querySelect == null || _.isEmpty(querySelect)) {
return;
}
this.setSelected(querySelect, columns);
}
private setSelected(querySelect: string[], columns: ISelectColumn[]): void {
this.clearAll();
querySelect &&
querySelect.forEach((value: string) => {
for (var i = 0; i < columns.length; i++) {
if (value === columns[i].columnName()) {
columns[i].selected(true);
}
}
});
}
public availableColumnsCheckboxClick(): boolean {
if (this.canSelectAll()) {
return this.selectAll();
} else {
return this.clearAll();
}
}
public selectAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: ISelectColumn) => {
value.selected(true);
});
return true;
}
public clearAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((column: ISelectColumn) => {
if (this.isEntityEditable(column.columnName())) {
column.selected(false);
} else {
column.selected(true);
}
});
return true;
}
public handleClick = (data: ISelectColumn, event: KeyboardEvent): boolean => {
this.selectTargetItem($(event.currentTarget), data);
return true;
};
private selectTargetItem($target: JQuery, targetColumn: ISelectColumn): void {
this.selectedColumnOption = targetColumn;
$(".list-item.selected").removeClass("selected");
$target.addClass("selected");
}
private isEntityEditable(name: string) {
if (this.queryViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
const cassandraKeys = this.queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
return !_.contains<string>(cassandraKeys, name);
}
return !(
name === Constants.EntityKeyNames.PartitionKey ||
name === Constants.EntityKeyNames.RowKey ||
name === Constants.EntityKeyNames.Timestamp
);
}
}

View File

@@ -1,15 +1,16 @@
import * as ko from "knockout"; import * as ko from "knockout";
import _ from "underscore"; import _ from "underscore";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; import { KeyCodes } from "../../../Common/Constants";
import * as Entities from "../../Tables/Entities"; import * as ViewModels from "../../../Contracts/ViewModels";
import EntityPropertyViewModel from "./EntityPropertyViewModel"; import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants"; import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel"; import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities"; import * as Utilities from "../../Tables/Utilities";
import * as ViewModels from "../../../Contracts/ViewModels";
import { KeyCodes } from "../../../Common/Constants";
import { ContextualPaneBase } from "../ContextualPaneBase"; import { ContextualPaneBase } from "../ContextualPaneBase";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
// Class with variables and functions that are common to both adding and editing entities // Class with variables and functions that are common to both adding and editing entities
export default abstract class TableEntityPane extends ContextualPaneBase { export default abstract class TableEntityPane extends ContextualPaneBase {
@@ -52,31 +53,29 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
constructor(options: ViewModels.PaneOptions) { constructor(options: ViewModels.PaneOptions) {
super(options); super(options);
this.container.isPreferredApiCassandra.subscribe((isCassandra) => { if (userContext.apiType === "Cassandra") {
if (isCassandra) { this.edmTypes([
this.edmTypes([ TableConstants.CassandraType.Text,
TableConstants.CassandraType.Text, TableConstants.CassandraType.Ascii,
TableConstants.CassandraType.Ascii, TableConstants.CassandraType.Bigint,
TableConstants.CassandraType.Bigint, TableConstants.CassandraType.Blob,
TableConstants.CassandraType.Blob, TableConstants.CassandraType.Boolean,
TableConstants.CassandraType.Boolean, TableConstants.CassandraType.Decimal,
TableConstants.CassandraType.Decimal, TableConstants.CassandraType.Double,
TableConstants.CassandraType.Double, TableConstants.CassandraType.Float,
TableConstants.CassandraType.Float, TableConstants.CassandraType.Int,
TableConstants.CassandraType.Int, TableConstants.CassandraType.Uuid,
TableConstants.CassandraType.Uuid, TableConstants.CassandraType.Varchar,
TableConstants.CassandraType.Varchar, TableConstants.CassandraType.Varint,
TableConstants.CassandraType.Varint, TableConstants.CassandraType.Inet,
TableConstants.CassandraType.Inet, TableConstants.CassandraType.Smallint,
TableConstants.CassandraType.Smallint, TableConstants.CassandraType.Tinyint,
TableConstants.CassandraType.Tinyint, ]);
]); }
}
});
this.canAdd = ko.computed<boolean>(() => { this.canAdd = ko.computed<boolean>(() => {
// Cassandra can't add since the schema can't be changed once created // Cassandra can't add since the schema can't be changed once created
if (this.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
return false; return false;
} }
// Adding '2' to the maximum to take into account PartitionKey and RowKey // Adding '2' to the maximum to take into account PartitionKey and RowKey
@@ -163,7 +162,7 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
public insertAttribute = (name?: string, type?: string): void => { public insertAttribute = (name?: string, type?: string): void => {
let entityProperty: EntityPropertyViewModel; let entityProperty: EntityPropertyViewModel;
if (!!name && !!type && this.container.isPreferredApiCassandra()) { if (!!name && !!type && userContext.apiType === "Cassandra") {
// TODO figure out validation story for blob and Inet so we can allow adding/editing them // TODO figure out validation story for blob and Inet so we can allow adding/editing them
const nonEditableType: boolean = const nonEditableType: boolean =
type === TableConstants.CassandraType.Blob || type === TableConstants.CassandraType.Inet; type === TableConstants.CassandraType.Blob || type === TableConstants.CassandraType.Inet;
@@ -253,8 +252,7 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
key !== TableEntityProcessor.keyProperties.etag && key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId && key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self && key !== TableEntityProcessor.keyProperties.self &&
(!viewModel.queryTablesTab.container.isPreferredApiCassandra() || (userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
key !== TableConstants.EntityKeyNames.RowKey)
) { ) {
newHeaders.push(key); newHeaders.push(key);
} }

View File

@@ -1,79 +0,0 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="queryselectpane">
<!-- Query Select form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Query Select header - Start -->
<div class="firstdivbg headerline">
Select Column
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keydown: onCloseKeyPress }"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Query Select header - End -->
<div class="paneMainContent paneContentContainer">
<!--<div class="row">
<label id="instructionLabel" data-bind="text: instructionLabel"></label>
</div>-->
<div><span>Select the columns that you want to query.</span></div>
<div class="column-options">
<div class="columns-border">
<input class="all-select-check" type="checkbox" data-bind="checked: allSelected" />
<label
style="font-weight: 700"
id="availableColumnsTableQueryLabel"
data-bind="text: availableColumnsTableQueryLabel"
></label>
</div>
<div class="content">
<section>
<ul data-bind="foreach: columnOptions" aria-labelledby="availableColumnsTableQueryLabel" tabindex="0">
<!-- ko template: {if: editable} -->
<li
class="list-item columns-border"
data-bind="attr: { title: columnName }, click: $parent.handleClick "
>
<input type="checkbox" data-bind="attr: { title: columnName }, checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
<!-- ko template: {ifnot: editable} -->
<li class="list-item columns-border" data-bind="attr: { title: columnName } ">
<input type="checkbox" disabled data-bind="checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
</ul>
</section>
</div>
</div>
<div class="row-label" data-bind="style: { visibility: anyColumnSelected() ? 'hidden': 'visible' }">
<label class="warning" role="alert" aria-atomic="true" data-bind="text: noColumnSelectedWarning"></label>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Query Select form - End -->
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
import { mount } from "enzyme";
import * as ko from "knockout";
import React from "react";
import Explorer from "../../../Explorer";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { TableQuerySelectPanel } from "./index";
describe("Table query select Panel", () => {
const fakeExplorer = {} as Explorer;
const fakeQueryViewModal = {} as QueryViewModel;
fakeQueryViewModal.columnOptions = ko.observableArray<string>([""]);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
queryViewModel: fakeQueryViewModal,
};
it("should render Default properly", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("Should exist availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.exists("#availableCheckbox")).toBe(true);
});
it("Should checked availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
id: "availableCheckbox",
label: "Available Columns",
checked: true,
onChange: expect.any(Function),
});
});
});

View File

@@ -0,0 +1,155 @@
import { Checkbox, Text } from "office-ui-fabric-react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import * as Constants from "../../../Tables/Constants";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../../GenericRightPaneComponent";
interface TableQuerySelectPanelProps {
explorer: Explorer;
closePanel: () => void;
queryViewModel: QueryViewModel;
}
interface ISelectColumn {
columnName: string;
selected: boolean;
editable: boolean;
}
export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps> = ({
explorer,
closePanel,
queryViewModel,
}: TableQuerySelectPanelProps): JSX.Element => {
const [columnOptions, setColumnOptions] = useState<ISelectColumn[]>([
{ columnName: "", selected: true, editable: false },
]);
const [isAvailableColumnChecked, setIsAvailableColumnChecked] = useState<boolean>(true);
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: "",
formErrorDetail: "",
id: "querySelectPane",
isExecuting: false,
title: "Select Column",
submitButtonText: "OK",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const submit = (): void => {
queryViewModel.selectText(getParameters());
queryViewModel.getSelectMessage();
closePanel();
};
const handleClick = (isChecked: boolean, selectedColumn: string): void => {
const columns = columnOptions.map((column) => {
if (column.columnName === selectedColumn) {
column.selected = isChecked;
return { ...column };
}
return { ...column };
});
canSelectAll();
setColumnOptions(columns);
};
useEffect(() => {
queryViewModel && setTableColumns(queryViewModel.columnOptions());
}, []);
const setTableColumns = (columnNames: string[]): void => {
const columns: ISelectColumn[] =
columnNames &&
columnNames.length &&
columnNames.map((value: string) => {
const columnOption: ISelectColumn = {
columnName: value,
selected: true,
editable: isEntityEditable(value),
};
return columnOption;
});
setColumnOptions(columns);
};
const isEntityEditable = (name: string): boolean => {
if (userContext.apiType === "Cassandra") {
const cassandraKeys = queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
return !cassandraKeys.includes(name);
}
return !(
name === Constants.EntityKeyNames.PartitionKey ||
name === Constants.EntityKeyNames.RowKey ||
name === Constants.EntityKeyNames.Timestamp
);
};
const availableColumnsCheckboxClick = (event: React.FormEvent<HTMLElement>, isChecked: boolean): void => {
setIsAvailableColumnChecked(isChecked);
selectClearAll(isChecked);
};
const selectClearAll = (isChecked: boolean): void => {
const columns: ISelectColumn[] = columnOptions.map((column: ISelectColumn) => {
if (isEntityEditable(column.columnName)) {
column.selected = isChecked;
return { ...column };
}
return { ...column };
});
setColumnOptions(columns);
};
const getParameters = (): string[] => {
const selectedColumns = columnOptions.filter((value: ISelectColumn) => value.selected === true);
const columns: string[] = selectedColumns.map((value: ISelectColumn) => {
const name: string = value.columnName;
return name;
});
return columns;
};
const canSelectAll = (): void => {
const canSelectAllColumn: boolean = columnOptions.some((value: ISelectColumn) => {
return !value.selected;
});
setIsAvailableColumnChecked(!canSelectAllColumn);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<Text>Select the columns that you want to query.</Text>
<div className="column-select-view">
<Checkbox
id="availableCheckbox"
label="Available Columns"
checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick}
/>
{columnOptions.map((column) => {
return (
<Checkbox
label={column.columnName}
onChange={(_event, isChecked: boolean) => handleClick(isChecked, column.columnName)}
key={column.columnName}
checked={column.selected}
disabled={!column.editable}
/>
);
})}
</div>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -91,21 +91,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -196,27 +181,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -507,21 +471,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -590,9 +539,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -632,27 +578,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],

View File

@@ -1,9 +1,9 @@
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "office-ui-fabric-react"; import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "office-ui-fabric-react";
import React, { ChangeEvent, FunctionComponent, useState } from "react"; import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload"; import { Upload } from "../../../Common/Upload";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { UploadDetails, UploadDetailsRecord } from "../../../workers/upload/definitions";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { getErrorMessage } from "../../Tables/Utilities"; import { getErrorMessage } from "../../Tables/Utilities";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent"; import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
@@ -13,12 +13,6 @@ export interface UploadItemsPaneProps {
closePanel: () => void; closePanel: () => void;
} }
interface IUploadFileData {
numSucceeded: number;
numFailed: number;
fileName: string;
}
const getTitle = (): string => { const getTitle = (): string => {
if (userContext.apiType === "Cassandra" || userContext.apiType === "Tables") { if (userContext.apiType === "Cassandra" || userContext.apiType === "Tables") {
return "Upload Tables"; return "Upload Tables";
@@ -54,7 +48,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
selectedCollection selectedCollection
?.uploadFiles(files) ?.uploadFiles(files)
.then( .then(
(uploadDetails: UploadDetails) => { (uploadDetails) => {
setUploadFileData(uploadDetails.data); setUploadFileData(uploadDetails.data);
setFiles(undefined); setFiles(undefined);
}, },
@@ -84,6 +78,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
onClose: closePanel, onClose: closePanel,
onSubmit, onSubmit,
}; };
const columns: IColumn[] = [ const columns: IColumn[] = [
{ {
key: "fileName", key: "fileName",
@@ -105,12 +100,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
}, },
]; ];
const _renderItemColumn = (item: IUploadFileData, index: number, column: IColumn) => { const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
switch (column.key) { switch (column.key) {
case "status": case "status":
return <span>{item.numSucceeded + " items created, " + item.numFailed + " errors"}</span>; return `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
default: default:
return <span>{item.fileName}</span>; return item.fileName;
} }
}; };

View File

@@ -92,21 +92,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -197,27 +182,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -508,21 +472,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -593,9 +542,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function], "isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
@@ -636,27 +582,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshAllDatabases": [Function], "refreshAllDatabases": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],

View File

@@ -50,10 +50,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
this.subscriptions = []; this.subscriptions = [];
} }
public shouldComponentUpdate() {
return this.container.tabsManager.openedTabs.length === 0;
}
public componentWillUnmount() { public componentWillUnmount() {
while (this.subscriptions.length) { while (this.subscriptions.length) {
this.subscriptions.pop().dispose(); this.subscriptions.pop().dispose();
@@ -62,7 +58,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() { public componentDidMount() {
this.subscriptions.push( this.subscriptions.push(
this.container.tabsManager.openedTabs.subscribe(() => this.setState({})),
this.container.selectedNode.subscribe(() => this.setState({})), this.container.selectedNode.subscribe(() => this.setState({})),
this.container.isNotebookEnabled.subscribe(() => this.setState({})) this.container.isNotebookEnabled.subscribe(() => this.setState({}))
); );
@@ -80,7 +75,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
const tipsItems = this.createTipsItems(); const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent; const onClearRecent = this.clearMostRecent;
return ( const formContainer = (jsx: JSX.Element) => (
<div className="connectExplorerContainer">
<form className="connectExplorerFormContainer">{jsx}</form>
</div>
);
return formContainer(
<div className="splashScreenContainer"> <div className="splashScreenContainer">
<div className="splashScreen"> <div className="splashScreen">
<div className="title"> <div className="title">
@@ -226,7 +227,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
} }
if (!this.container.isDatabaseNodeOrNoneSelected()) { if (!this.container.isDatabaseNodeOrNoneSelected()) {
if (this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph()) { if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({ items.push({
iconSrc: NewQueryIcon, iconSrc: NewQueryIcon,
onClick: () => { onClick: () => {
@@ -255,7 +256,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
onClick: () => this.container.openBrowseQueriesPanel(), onClick: () => this.container.openBrowseQueriesPanel(),
}); });
if (!this.container.isPreferredApiCassandra()) { if (userContext.apiType !== "Cassandra") {
items.push({ items.push({
iconSrc: NewStoredProcedureIcon, iconSrc: NewStoredProcedureIcon,
title: "New Stored Procedure", title: "New Stored Procedure",

View File

@@ -1,4 +1,5 @@
import Q from "q"; import Q from "q";
import { userContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import * as DataTableUtilities from "./DataTableUtilities"; import * as DataTableUtilities from "./DataTableUtilities";
@@ -73,7 +74,7 @@ export default class TableCommands {
} }
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
let deleteMessage: string = "Are you sure you want to delete the selected entities?"; let deleteMessage: string = "Are you sure you want to delete the selected entities?";
if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
deleteMessage = "Are you sure you want to delete the selected rows?"; deleteMessage = "Are you sure you want to delete the selected rows?";
} }
if (window.confirm(deleteMessage)) { if (window.confirm(deleteMessage)) {

View File

@@ -5,6 +5,7 @@ import { Areas } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
@@ -412,10 +413,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
} }
var entities = this.cache.data; var entities = this.cache.data;
if ( if (userContext.apiType === "Cassandra" && DataTableUtilities.checkForDefaultHeader(this.headers)) {
this.queryTablesTab.container.isPreferredApiCassandra() &&
DataTableUtilities.checkForDefaultHeader(this.headers)
) {
(<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient) (<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient)
.getTableSchema(this.queryTablesTab.collection) .getTableSchema(this.queryTablesTab.collection)
.then((headers: CassandraTableKey[]) => { .then((headers: CassandraTableKey[]) => {
@@ -427,7 +425,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
} else { } else {
var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities( var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
entities, entities,
this.queryTablesTab.container.isPreferredApiCassandra() userContext.apiType === "Cassandra"
); );
var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers); var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers);
if (newHeaders.length > 0) { if (newHeaders.length > 0) {
@@ -512,7 +510,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return Q.resolve(finalEntities); return Q.resolve(finalEntities);
} }
); );
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { } else if (this.continuationToken && userContext.apiType === "Cassandra") {
promise = Q( promise = Q(
this.queryTablesTab.container.tableDataClient.queryDocuments( this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection, this.queryTablesTab.collection,
@@ -523,7 +521,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
); );
} else { } else {
let query = this.sqlQuery(); let query = this.sqlQuery();
if (this.queryTablesTab.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
query = this.cqlQuery(); query = this.cqlQuery();
} }
promise = Q( promise = Q(

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities"; import * as DataTableUtilities from "../DataTable/DataTableUtilities";
@@ -70,7 +71,7 @@ export default class QueryBuilderViewModel {
private scrollEventListener: boolean; private scrollEventListener: boolean;
constructor(queryViewModel: QueryViewModel, tableEntityListViewModel: TableEntityListViewModel) { constructor(queryViewModel: QueryViewModel, tableEntityListViewModel: TableEntityListViewModel) {
if (tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
this.edmTypes([ this.edmTypes([
Constants.CassandraType.Text, Constants.CassandraType.Text,
Constants.CassandraType.Ascii, Constants.CassandraType.Ascii,

View File

@@ -1,9 +1,10 @@
import * as ko from "knockout"; import * as ko from "knockout";
import _ from "underscore"; import _ from "underscore";
import { userContext } from "../../../UserContext";
import * as QueryBuilderConstants from "../Constants"; import * as QueryBuilderConstants from "../Constants";
import QueryBuilderViewModel from "./QueryBuilderViewModel";
import ClauseGroup from "./ClauseGroup";
import * as Utilities from "../Utilities"; import * as Utilities from "../Utilities";
import ClauseGroup from "./ClauseGroup";
import QueryBuilderViewModel from "./QueryBuilderViewModel";
export default class QueryClauseViewModel { export default class QueryClauseViewModel {
public checkedForGrouping: ko.Observable<boolean>; public checkedForGrouping: ko.Observable<boolean>;
@@ -68,7 +69,7 @@ export default class QueryClauseViewModel {
this.getValueType(); this.getValueType();
this.isOperaterEditable = ko.pureComputed<boolean>(() => { this.isOperaterEditable = ko.pureComputed<boolean>(() => {
const isPreferredApiCassandra = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra(); const isPreferredApiCassandra = userContext.apiType === "Cassandra";
const cassandraKeys = isPreferredApiCassandra const cassandraKeys = isPreferredApiCassandra
? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map( ? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map(
(key) => key.property (key) => key.property
@@ -84,7 +85,7 @@ export default class QueryClauseViewModel {
this.field() !== "Timestamp" && this.field() !== "Timestamp" &&
this.field() !== "PartitionKey" && this.field() !== "PartitionKey" &&
this.field() !== "RowKey" && this.field() !== "RowKey" &&
!this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra() userContext.apiType !== "Cassandra"
); );
this.and_or.subscribe((value) => { this.and_or.subscribe((value) => {
@@ -170,7 +171,7 @@ export default class QueryClauseViewModel {
this.type(QueryBuilderConstants.TableType.String); this.type(QueryBuilderConstants.TableType.String);
} else { } else {
this.resetFromTimestamp(); this.resetFromTimestamp();
if (this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection
.cassandraSchema; .cassandraSchema;
for (let i = 0, len = cassandraSchema.length; i < len; i++) { for (let i = 0, len = cassandraSchema.length; i < len; i++) {

View File

@@ -1,13 +1,13 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import { KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import QueryBuilderViewModel from "./QueryBuilderViewModel"; import QueryBuilderViewModel from "./QueryBuilderViewModel";
import QueryClauseViewModel from "./QueryClauseViewModel"; import QueryClauseViewModel from "./QueryClauseViewModel";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import { KeyCodes } from "../../../Common/Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
export default class QueryViewModel { export default class QueryViewModel {
public topValueLimitMessage: string = "Please input a number between 0 and 1000."; public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
@@ -47,7 +47,7 @@ export default class QueryViewModel {
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel(); this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel();
this.queryTextIsReadOnly = ko.computed<boolean>(() => { this.queryTextIsReadOnly = ko.computed<boolean>(() => {
return !this.queryTablesTab.container.isPreferredApiCassandra(); return userContext.apiType !== "Cassandra";
}); });
let initialOptions = this._tableEntityListViewModel.headers; let initialOptions = this._tableEntityListViewModel.headers;
this.columnOptions = ko.observableArray<string>(initialOptions); this.columnOptions = ko.observableArray<string>(initialOptions);
@@ -127,7 +127,7 @@ export default class QueryViewModel {
private setFilter = (): string => { private setFilter = (): string => {
var queryString = this.isEditorActive() var queryString = this.isEditorActive()
? this.queryText() ? this.queryText()
: this.queryTablesTab.container.isPreferredApiCassandra() : userContext.apiType === "Cassandra"
? this.queryBuilderViewModel().getCqlFilterFromClauses() ? this.queryBuilderViewModel().getCqlFilterFromClauses()
: this.queryBuilderViewModel().getODataFilterFromClauses(); : this.queryBuilderViewModel().getODataFilterFromClauses();
var filter = queryString; var filter = queryString;
@@ -160,7 +160,7 @@ export default class QueryViewModel {
public runQuery = (): DataTables.DataTable => { public runQuery = (): DataTables.DataTable => {
var filter = this.setFilter(); var filter = this.setFilter();
if (filter && !this.queryTablesTab.container.isPreferredApiCassandra()) { if (filter && userContext.apiType !== "Cassandra") {
filter = filter.replace(/"/g, "'"); filter = filter.replace(/"/g, "'");
} }
var top = this.topValue(); var top = this.topValue();
@@ -198,8 +198,7 @@ export default class QueryViewModel {
}; };
public selectQueryOptions(): Promise<any> { public selectQueryOptions(): Promise<any> {
this.queryTablesTab.container.querySelectPane.queryViewModel = this; this.queryTablesTab.container.openTableSelectQueryPanel(this);
this.queryTablesTab.container.querySelectPane.open();
return null; return null;
} }

View File

@@ -16,8 +16,6 @@ describe("Documents tab", () => {
title: "", title: "",
tabPath: "", tabPath: "",
hashLocation: "", hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
}); });
@@ -89,8 +87,6 @@ describe("Documents tab", () => {
title: "", title: "",
tabPath: "", tabPath: "",
hashLocation: "", hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
}); });
@@ -106,8 +102,6 @@ describe("Documents tab", () => {
title: "", title: "",
tabPath: "", tabPath: "",
hashLocation: "", hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
}); });
@@ -123,8 +117,6 @@ describe("Documents tab", () => {
title: "", title: "",
tabPath: "", tabPath: "",
hashLocation: "", hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
}); });
@@ -140,8 +132,6 @@ describe("Documents tab", () => {
title: "", title: "",
tabPath: "", tabPath: "",
hashLocation: "", hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
}); });
@@ -157,8 +147,6 @@ describe("Documents tab", () => {
title: "", title: "",
tabPath: "", tabPath: "",
hashLocation: "", hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
}); });

View File

@@ -1,14 +1,9 @@
import * as Constants from "../../Common/Constants"; import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
import MongoUtility from "../../Common/MongoUtility";
import ObjectId from "../Tree/ObjectId";
import Q from "q"; import Q from "q";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as Constants from "../../Common/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { import {
createDocument, createDocument,
deleteDocument, deleteDocument,
@@ -16,10 +11,14 @@ import {
readDocument, readDocument,
updateDocument, updateDocument,
} from "../../Common/MongoProxyClient"; } from "../../Common/MongoProxyClient";
import { extractPartitionKey } from "@azure/cosmos"; import MongoUtility from "../../Common/MongoUtility";
import * as Logger from "../../Common/Logger"; import * as DataModels from "../../Contracts/DataModels";
import { PartitionKeyDefinition } from "@azure/cosmos"; import * as ViewModels from "../../Contracts/ViewModels";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId";
import DocumentsTab from "./DocumentsTab";
export default class MongoDocumentsTab extends DocumentsTab { export default class MongoDocumentsTab extends DocumentsTab {
public collection: ViewModels.Collection; public collection: ViewModels.Collection;

View File

@@ -88,7 +88,6 @@ export default class NotebookTabV2 extends TabsBase {
public onCloseTabButtonClick(): Q.Promise<any> { public onCloseTabButtonClick(): Q.Promise<any> {
const cleanup = () => { const cleanup = () => {
this.notebookComponentAdapter.notebookShutdown(); this.notebookComponentAdapter.notebookShutdown();
this.isActive(false);
super.onCloseTabButtonClick(); super.onCloseTabButtonClick();
}; };

View File

@@ -1,9 +1,10 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import QueryTab from "./QueryTab"; import QueryTab from "./QueryTab";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
describe("Query Tab", () => { describe("Query Tab", () => {
function getNewQueryTabForContainer(container: Explorer): QueryTab { function getNewQueryTabForContainer(container: Explorer): QueryTab {
@@ -24,7 +25,6 @@ describe("Query Tab", () => {
database: database, database: database,
title: "", title: "",
tabPath: "", tabPath: "",
isActive: ko.observable<boolean>(false),
hashLocation: "", hashLocation: "",
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}, onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
}); });
@@ -52,13 +52,19 @@ describe("Query Tab", () => {
}); });
it("should be true for accounts using SQL API", () => { it("should be true for accounts using SQL API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase()); updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer); const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(true); expect(queryTab.isQueryMetricsEnabled()).toBe(true);
}); });
it("should be false for accounts using other APIs", () => { it("should be false for accounts using other APIs", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase()); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer); const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.isQueryMetricsEnabled()).toBe(false); expect(queryTab.isQueryMetricsEnabled()).toBe(false);
}); });
@@ -72,13 +78,19 @@ describe("Query Tab", () => {
}); });
it("should be visible when using a supported API", () => { it("should be visible when using a supported API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB); updateUserContext({});
const queryTab = getNewQueryTabForContainer(explorer); const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(true); expect(queryTab.saveQueryButton.visible()).toBe(true);
}); });
it("should not be visible when using an unsupported API", () => { it("should not be visible when using an unsupported API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const queryTab = getNewQueryTabForContainer(explorer); const queryTab = getNewQueryTabForContainer(explorer);
expect(queryTab.saveQueryButton.visible()).toBe(false); expect(queryTab.saveQueryButton.visible()).toBe(false);
}); });

View File

@@ -13,6 +13,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as QueryUtils from "../../Utils/QueryUtils"; import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import template from "./QueryTab.html"; import template from "./QueryTab.html";
@@ -95,9 +96,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)) this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))
); );
this.isQueryMetricsEnabled = ko.computed<boolean>(() => { this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
return ( return userContext.apiType === "SQL" || false;
(this.collection && this.collection.container && this.collection.container.isPreferredApiDocumentDB()) || false
);
}); });
this.activityId = ko.observable<string>(); this.activityId = ko.observable<string>();
this.roundTrips = ko.observable<number>(); this.roundTrips = ko.observable<number>();
@@ -117,7 +116,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this._isSaveQueriesEnabled = ko.computed<boolean>(() => { this._isSaveQueriesEnabled = ko.computed<boolean>(() => {
const container = this.collection && this.collection.container; const container = this.collection && this.collection.container;
return (container && (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph())) || false; return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
}); });
this.maybeSubQuery = ko.computed<boolean>(function () { this.maybeSubQuery = ko.computed<boolean>(function () {

View File

@@ -1,21 +1,21 @@
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels"; import AddEntityIcon from "../../../images/AddEntity.svg";
import TabsBase from "./TabsBase"; import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel"; import EditEntityIcon from "../../../images/Edit-entity.svg";
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel"; import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import TableCommands from "../Tables/DataTable/TableCommands";
import { TableDataClient } from "../Tables/TableDataClient";
import QueryBuilderIcon from "../../../images/Query-Builder.svg"; import QueryBuilderIcon from "../../../images/Query-Builder.svg";
import QueryTextIcon from "../../../images/Query-Text.svg"; import QueryTextIcon from "../../../images/Query-Text.svg";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; import * as ViewModels from "../../Contracts/ViewModels";
import AddEntityIcon from "../../../images/AddEntity.svg"; import { userContext } from "../../UserContext";
import EditEntityIcon from "../../../images/Edit-entity.svg";
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
import Explorer from "../Explorer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import TableCommands from "../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
import { TableDataClient } from "../Tables/TableDataClient";
import template from "./QueryTablesTab.html"; import template from "./QueryTablesTab.html";
import TabsBase from "./TabsBase";
// Will act as table explorer class // Will act as table explorer class
export default class QueryTablesTab extends TabsBase { export default class QueryTablesTab extends TabsBase {
@@ -176,7 +176,7 @@ export default class QueryTablesTab extends TabsBase {
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
if (this.queryBuilderButton.visible()) { if (this.queryBuilderButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "CQL Query Builder" : "Query Builder"; const label = userContext.apiType === "Cassandra" ? "CQL Query Builder" : "Query Builder";
buttons.push({ buttons.push({
iconSrc: QueryBuilderIcon, iconSrc: QueryBuilderIcon,
iconAlt: label, iconAlt: label,
@@ -190,7 +190,7 @@ export default class QueryTablesTab extends TabsBase {
} }
if (this.queryTextButton.visible()) { if (this.queryTextButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "CQL Query Text" : "Query Text"; const label = userContext.apiType === "Cassandra" ? "CQL Query Text" : "Query Text";
buttons.push({ buttons.push({
iconSrc: QueryTextIcon, iconSrc: QueryTextIcon,
iconAlt: label, iconAlt: label,
@@ -217,7 +217,7 @@ export default class QueryTablesTab extends TabsBase {
} }
if (this.addEntityButton.visible()) { if (this.addEntityButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "Add Row" : "Add Entity"; const label = userContext.apiType === "Cassandra" ? "Add Row" : "Add Entity";
buttons.push({ buttons.push({
iconSrc: AddEntityIcon, iconSrc: AddEntityIcon,
iconAlt: label, iconAlt: label,
@@ -230,7 +230,7 @@ export default class QueryTablesTab extends TabsBase {
} }
if (this.editEntityButton.visible()) { if (this.editEntityButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "Edit Row" : "Edit Entity"; const label = userContext.apiType === "Cassandra" ? "Edit Row" : "Edit Entity";
buttons.push({ buttons.push({
iconSrc: EditEntityIcon, iconSrc: EditEntityIcon,
iconAlt: label, iconAlt: label,
@@ -243,7 +243,7 @@ export default class QueryTablesTab extends TabsBase {
} }
if (this.deleteEntityButton.visible()) { if (this.deleteEntityButton.visible()) {
const label = this.container.isPreferredApiCassandra() ? "Delete Rows" : "Delete Entities"; const label = userContext.apiType === "Cassandra" ? "Delete Rows" : "Delete Entities";
buttons.push({ buttons.push({
iconSrc: DeleteEntitiesIcon, iconSrc: DeleteEntitiesIcon,
iconAlt: label, iconAlt: label,

View File

@@ -208,7 +208,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
this.collection && this.collection.container.openExecuteSprocParamsPanel(); this.collection && this.collection.container.openExecuteSprocParamsPanel(this.node);
}, },
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@@ -1,15 +1,16 @@
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { RouteHandler } from "../../RouteHandlers/RouteHandler";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as ThemeUtility from "../../Common/ThemeUtility"; import * as ThemeUtility from "../../Common/ThemeUtility";
import Explorer from "../Explorer"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { RouteHandler } from "../../RouteHandlers/RouteHandler";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel { export default class TabsBase extends WaitsForTemplateViewModel {
@@ -20,18 +21,16 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public database: ViewModels.Database; public database: ViewModels.Database;
public rid: string; public rid: string;
public hasFocus: ko.Observable<boolean>; public hasFocus: ko.Observable<boolean>;
public isActive: ko.Observable<boolean>;
public isMouseOver: ko.Observable<boolean>; public isMouseOver: ko.Observable<boolean>;
public tabId: string; public tabId: string;
public tabKind: ViewModels.CollectionTabKind; public tabKind: ViewModels.CollectionTabKind;
public tabTitle: ko.Observable<string>; public tabTitle: ko.Observable<string>;
public tabPath: ko.Observable<string>; public tabPath: ko.Observable<string>;
public closeButtonTabIndex: ko.Computed<number>;
public errorDetailsTabIndex: ko.Computed<number>;
public hashLocation: ko.Observable<string>; public hashLocation: ko.Observable<string>;
public isExecutionError: ko.Observable<boolean>; public isExecutionError: ko.Observable<boolean>;
public isExecuting: ko.Observable<boolean>; public isExecuting: ko.Observable<boolean>;
public pendingNotification?: ko.Observable<DataModels.Notification>; public pendingNotification?: ko.Observable<DataModels.Notification>;
public manager?: TabsManager;
protected _theme: string; protected _theme: string;
public onLoadStartKey: number; public onLoadStartKey: number;
@@ -46,7 +45,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
this.database = options.database; this.database = options.database;
this.rid = options.rid || (this.collection && this.collection.rid) || ""; this.rid = options.rid || (this.collection && this.collection.rid) || "";
this.hasFocus = ko.observable<boolean>(false); this.hasFocus = ko.observable<boolean>(false);
this.isActive = options.isActive || ko.observable<boolean>(false);
this.isMouseOver = ko.observable<boolean>(false); this.isMouseOver = ko.observable<boolean>(false);
this.tabId = `tab${id}`; this.tabId = `tab${id}`;
this.tabKind = options.tabKind; this.tabKind = options.tabKind;
@@ -55,21 +53,12 @@ export default class TabsBase extends WaitsForTemplateViewModel {
(options.tabPath && ko.observable<string>(options.tabPath)) || (options.tabPath && ko.observable<string>(options.tabPath)) ||
(this.collection && (this.collection &&
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`)); ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
this.closeButtonTabIndex = ko.computed<number>(() => (this.isActive() ? 0 : null));
this.errorDetailsTabIndex = ko.computed<number>(() => (this.isActive() ? 0 : null));
this.isExecutionError = ko.observable<boolean>(false); this.isExecutionError = ko.observable<boolean>(false);
this.isExecuting = ko.observable<boolean>(false); this.isExecuting = ko.observable<boolean>(false);
this.pendingNotification = ko.observable<DataModels.Notification>(undefined); this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
this.onLoadStartKey = options.onLoadStartKey; this.onLoadStartKey = options.onLoadStartKey;
this.hashLocation = ko.observable<string>(options.hashLocation || ""); this.hashLocation = ko.observable<string>(options.hashLocation || "");
this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation)); this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation));
this.isActive.subscribe((isActive: boolean) => {
if (isActive) {
this.onActivate();
}
});
this.closeTabButton = { this.closeTabButton = {
enabled: ko.computed<boolean>(() => { enabled: ko.computed<boolean>(() => {
return true; return true;
@@ -82,12 +71,9 @@ export default class TabsBase extends WaitsForTemplateViewModel {
} }
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
const explorer = this.getContainer(); this.manager?.closeTab(this);
explorer.tabsManager.closeTab(this.tabId, explorer);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
tabName: this.constructor.name, tabName: this.constructor.name,
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(), tabTitle: this.tabTitle(),
tabId: this.tabId, tabId: this.tabId,
@@ -95,7 +81,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
} }
public onTabClick(): void { public onTabClick(): void {
this.getContainer().tabsManager.activateTab(this); this.manager?.activateTab(this);
} }
protected updateSelectedNode(): void { protected updateSelectedNode(): void {
@@ -127,6 +113,11 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick()); return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
}; };
/** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
public isActive() {
return this === this.manager?.activeTab();
}
public onActivate(): void { public onActivate(): void {
this.updateSelectedNode(); this.updateSelectedNode();
if (!!this.collection) { if (!!this.collection) {

View File

@@ -1,4 +1,8 @@
<div id="content" class="flexContainer hideOverflows" data-bind="visible: openedTabs && openedTabs().length > 0"> <div
id="content"
class="flexContainer hideOverflows"
data-bind="visible: activeTab() && openedTabs && openedTabs().length > 0"
>
<!-- Tabs - Start --> <!-- Tabs - Start -->
<div class="nav-tabs-margin"> <div class="nav-tabs-margin">
<ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist"> <ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
@@ -8,12 +12,12 @@
data-bind=" data-bind="
attr: { attr: {
title: $data.tabPath, title: $data.tabPath,
'aria-selected': $data.isActive, 'aria-selected': $parent.activeTab() === $data,
'aria-expanded': $data.isActive, 'aria-expanded': $parent.activeTab() === $data,
'aria-controls': $data.tabId 'aria-controls': $data.tabId
}, },
css:{ css:{
active: $data.isActive active: $parent.activeTab() === $data
}, },
hasFocus: $data.hasFocus, hasFocus: $data.hasFocus,
event: { keypress: onKeyPressActivate }, event: { keypress: onKeyPressActivate },
@@ -33,8 +37,8 @@
data-bind=" data-bind="
click: onErrorDetailsClick, click: onErrorDetailsClick,
event: { keypress: onErrorDetailsKeyPress }, event: { keypress: onErrorDetailsKeyPress },
attr: { tabindex: errorDetailsTabIndex }, attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
css: { actionsEnabled: isActive }, css: { actionsEnabled: $parent.activeTab() === $data },
visible: isExecutionError" visible: isExecutionError"
> >
<span class="errorIcon"></span> <span class="errorIcon"></span>
@@ -56,11 +60,14 @@
data-bind=" data-bind="
click: $data.onCloseTabButtonClick, click: $data.onCloseTabButtonClick,
event: { keypress: onKeyPressClose }, event: { keypress: onKeyPressClose },
attr: { tabindex: $data.closeButtonTabIndex }, attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
visible: $data.isActive() || $data.isMouseOver()" visible: $parent.activeTab() === $data || $data.isMouseOver()"
title="Close" title="Close"
> >
<span class="tabIcon close-Icon" data-bind="visible: $data.isActive() || $data.isMouseOver()"> <span
class="tabIcon close-Icon"
data-bind="visible: $parent.activeTab() === $data || $data.isMouseOver()"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" /> <img src="../../../images/close-black.svg" title="Close" alt="Close" />
</span> </span>
</span> </span>
@@ -77,7 +84,7 @@
<!-- Tabs Panes -- Start --> <!-- Tabs Panes -- Start -->
<div class="tabPanesContainer"> <div class="tabPanesContainer">
<!-- ko foreach: openedTabs --> <!-- ko foreach: openedTabs -->
<div class="tabs-container" data-bind="visible: $data.isActive"> <div class="tabs-container" data-bind="visible: $parent.activeTab() === $data">
<span <span
data-bind="class: $data.constructor.component.name, component: { name: $data.constructor.component.name, params: $data }" data-bind="class: $data.constructor.component.name, component: { name: $data.constructor.component.name, params: $data }"
></span> ></span>

View File

@@ -1,11 +1,11 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { TabsManager } from "./TabsManager"; import * as ViewModels from "../../Contracts/ViewModels";
import DocumentsTab from "./DocumentsTab";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import QueryTab from "./QueryTab";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
import DocumentsTab from "./DocumentsTab";
import QueryTab from "./QueryTab";
import { TabsManager } from "./TabsManager";
describe("Tabs manager tests", () => { describe("Tabs manager tests", () => {
let tabsManager: TabsManager; let tabsManager: TabsManager;
@@ -50,7 +50,6 @@ describe("Tabs manager tests", () => {
database, database,
title: "", title: "",
tabPath: "", tabPath: "",
isActive: ko.observable<boolean>(false),
hashLocation: "", hashLocation: "",
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
}); });
@@ -63,7 +62,6 @@ describe("Tabs manager tests", () => {
title: "", title: "",
tabPath: "", tabPath: "",
hashLocation: "", hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
}); });
@@ -72,10 +70,7 @@ describe("Tabs manager tests", () => {
documentsTab.tabId = "2"; documentsTab.tabId = "2";
}); });
beforeEach(() => { beforeEach(() => (tabsManager = new TabsManager()));
tabsManager = new TabsManager();
explorer.tabsManager = tabsManager;
});
it("open new tabs", () => { it("open new tabs", () => {
tabsManager.activateNewTab(queryTab); tabsManager.activateNewTab(queryTab);
@@ -122,7 +117,7 @@ describe("Tabs manager tests", () => {
tabsManager.activateNewTab(queryTab); tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab); tabsManager.activateNewTab(documentsTab);
tabsManager.closeTab(documentsTab.tabId, explorer); tabsManager.closeTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(1); expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab); expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab); expect(tabsManager.activeTab()).toEqual(queryTab);

View File

@@ -1,16 +1,10 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
export class TabsManager { export class TabsManager {
public openedTabs: ko.ObservableArray<TabsBase>; public openedTabs = ko.observableArray<TabsBase>([]);
public activeTab: ko.Observable<TabsBase>; public activeTab = ko.observable<TabsBase>();
constructor() {
this.openedTabs = ko.observableArray<TabsBase>([]);
this.activeTab = ko.observable<TabsBase>();
}
public activateNewTab(tab: TabsBase): void { public activateNewTab(tab: TabsBase): void {
this.openedTabs.push(tab); this.openedTabs.push(tab);
@@ -18,66 +12,43 @@ export class TabsManager {
} }
public activateTab(tab: TabsBase): void { public activateTab(tab: TabsBase): void {
this.activeTab() && this.activeTab().isActive(false); if (this.openedTabs().includes(tab)) {
tab.isActive(true); tab.manager = this;
this.activeTab(tab); this.activeTab(tab);
tab.onActivate();
}
} }
public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] { public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] {
return this.openedTabs().filter((openedTab: TabsBase) => { return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab)));
return openedTab.tabKind === tabKind && (!comparator || comparator(openedTab));
});
} }
public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void { public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state // ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
this.openedTabs().forEach((tab: TabsBase) => { this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate();
if (comparator(tab) && tab.isActive()) {
tab.onActivate();
}
});
}
public removeTabById(tabId: string): void {
this.openedTabs.remove((tab: TabsBase) => tab.tabId === tabId);
}
public removeTabByComparator(comparator: (tab: TabsBase) => boolean): void {
this.openedTabs.remove((tab: TabsBase) => comparator(tab));
} }
public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void { public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void {
this.activeTab() && this.activeTab().isActive(false); this.openedTabs()
this.activeTab(undefined); .filter(comparator)
this.openedTabs().forEach((tab: TabsBase) => { .forEach((tab) => tab.onCloseTabButtonClick());
if (comparator(tab)) {
tab.onCloseTabButtonClick();
}
});
} }
public closeTabs(): void { public closeTab(tab: TabsBase): void {
this.openedTabs([]); const tabIndex = this.openedTabs().indexOf(tab);
}
public closeTab(tabId: string, explorer: Explorer): void {
const tabIndex: number = this.openedTabs().findIndex((tab: TabsBase) => tab.tabId === tabId);
if (tabIndex !== -1) { if (tabIndex !== -1) {
const tabToActive: TabsBase = this.openedTabs()[tabIndex + 1] || this.openedTabs()[tabIndex - 1]; this.openedTabs.remove(tab);
this.openedTabs()[tabIndex].isActive(false); tab.manager = undefined;
this.removeTabById(tabId);
if (tabToActive) { if (this.openedTabs().length === 0) {
tabToActive.isActive(true);
this.activeTab(tabToActive);
} else {
explorer.selectedNode(undefined);
explorer.onUpdateTabsButtons([]);
this.activeTab(undefined); this.activeTab(undefined);
} }
if (tab === this.activeTab()) {
const tabToTheRight = this.openedTabs()[tabIndex];
const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1];
this.activateTab(tabToTheRight ?? lastOpenTab);
}
} }
} }
public isTabActive(tabKind: ViewModels.CollectionTabKind): boolean {
return this.activeTab() && this.activeTab().tabKind === tabKind;
}
} }

View File

@@ -1,8 +1,7 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as DataModels from "../../Contracts/DataModels";
import Collection from "./Collection";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import Collection from "./Collection";
jest.mock("monaco-editor"); jest.mock("monaco-editor");
describe("Collection", () => { describe("Collection", () => {
@@ -35,18 +34,11 @@ describe("Collection", () => {
mockContainer.isPreferredApiMongoDB = ko.computed(() => { mockContainer.isPreferredApiMongoDB = ko.computed(() => {
return false; return false;
}); });
mockContainer.isPreferredApiCassandra = ko.computed(() => {
return false;
});
mockContainer.isDatabaseNodeOrNoneSelected = () => { mockContainer.isDatabaseNodeOrNoneSelected = () => {
return false; return false;
}; };
mockContainer.isPreferredApiDocumentDB = ko.computed(() => {
return true;
});
mockContainer.isPreferredApiGraph = ko.computed(() => {
return false;
});
mockContainer.deleteCollectionText = ko.observable<string>("delete collection"); mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer); return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);

View File

@@ -1,9 +1,8 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import UploadWorker from "worker-loader!../../workers/upload";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
@@ -13,16 +12,14 @@ import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefine
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { configContext, Platform } from "../../ConfigContext";
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 { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import ConflictsTab from "../Tabs/ConflictsTab"; import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
@@ -196,7 +193,7 @@ export default class Collection implements ViewModels.Collection {
.map((node) => <Trigger>node); .map((node) => <Trigger>node);
}); });
const showScriptsMenus: boolean = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); const showScriptsMenus: boolean = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus); this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus); this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus); this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);
@@ -304,7 +301,6 @@ export default class Collection implements ViewModels.Collection {
documentIds: ko.observableArray<DocumentId>([]), documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
title: "Items", title: "Items",
isActive: ko.observable<boolean>(false),
collection: this, collection: this,
node: this, node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`, tabPath: `${this.databaseId}>${this.id()}>Documents`,
@@ -352,7 +348,6 @@ export default class Collection implements ViewModels.Collection {
conflictIds: ko.observableArray<ConflictId>([]), conflictIds: ko.observableArray<ConflictId>([]),
tabKind: ViewModels.CollectionTabKind.Conflicts, tabKind: ViewModels.CollectionTabKind.Conflicts,
title: "Conflicts", title: "Conflicts",
isActive: ko.observable<boolean>(false),
collection: this, collection: this,
node: this, node: this,
tabPath: `${this.databaseId}>${this.id()}>Conflicts`, tabPath: `${this.databaseId}>${this.id()}>Conflicts`,
@@ -377,7 +372,7 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
if (this.container.isPreferredApiCassandra() && !this.cassandraKeys) { if (userContext.apiType === "Cassandra" && !this.cassandraKeys) {
(<CassandraAPIDataClient>this.container.tableDataClient).getTableKeys(this).then((keys: CassandraTableKeys) => { (<CassandraAPIDataClient>this.container.tableDataClient).getTableKeys(this).then((keys: CassandraTableKeys) => {
this.cassandraKeys = keys; this.cassandraKeys = keys;
}); });
@@ -394,7 +389,7 @@ export default class Collection implements ViewModels.Collection {
} else { } else {
this.documentIds([]); this.documentIds([]);
let title = `Entities`; let title = `Entities`;
if (this.container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
title = `Rows`; title = `Rows`;
} }
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
@@ -409,12 +404,9 @@ export default class Collection implements ViewModels.Collection {
tabKind: ViewModels.CollectionTabKind.QueryTables, tabKind: ViewModels.CollectionTabKind.QueryTables,
title: title, title: title,
tabPath: "", tabPath: "",
collection: this, collection: this,
node: this, node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`,
isActive: ko.observable(false),
onLoadStartKey: startKey, onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}); });
@@ -466,7 +458,6 @@ export default class Collection implements ViewModels.Collection {
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(),
isActive: ko.observable(false),
databaseId: this.databaseId, databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded, isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey, onLoadStartKey: startKey,
@@ -513,12 +504,9 @@ export default class Collection implements ViewModels.Collection {
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
title: "Documents", title: "Documents",
tabPath: "", tabPath: "",
collection: this, collection: this,
node: this, node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`,
isActive: ko.observable(false),
onLoadStartKey: startKey, onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}); });
@@ -561,7 +549,6 @@ export default class Collection implements ViewModels.Collection {
collection: this, collection: this,
node: this, node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}; };
@@ -604,7 +591,6 @@ export default class Collection implements ViewModels.Collection {
collection: this, collection: this,
node: this, node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
isActive: ko.observable(false),
queryText: queryText, queryText: queryText,
partitionKey: collection.partitionKey, partitionKey: collection.partitionKey,
onLoadStartKey: startKey, onLoadStartKey: startKey,
@@ -634,7 +620,6 @@ export default class Collection implements ViewModels.Collection {
collection: this, collection: this,
node: this, node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`,
isActive: ko.observable(false),
partitionKey: collection.partitionKey, partitionKey: collection.partitionKey,
onLoadStartKey: startKey, onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
@@ -666,7 +651,6 @@ export default class Collection implements ViewModels.Collection {
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(),
isActive: ko.observable(false),
databaseId: this.databaseId, databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded, isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey, onLoadStartKey: startKey,
@@ -685,7 +669,6 @@ export default class Collection implements ViewModels.Collection {
collection: this, collection: this,
node: this, node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}); });
@@ -960,73 +943,6 @@ export default class Collection implements ViewModels.Collection {
this.uploadFiles(event.originalEvent.dataTransfer.files); this.uploadFiles(event.originalEvent.dataTransfer.files);
} }
public uploadFiles = (fileList: FileList): Promise<UploadDetails> => {
// TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability
if (configContext.platform === Platform.Hosted && userContext.authType === AuthType.AAD) {
return this._uploadFilesCors(fileList);
}
const documentUploader: Worker = new UploadWorker();
let inProgressNotificationId: string = "";
if (!fileList || fileList.length === 0) {
return Promise.reject("No files specified");
}
const onmessage = (resolve: (value: UploadDetails) => void, reject: (reason: any) => void, event: MessageEvent) => {
const numSuccessful: number = event.data.numUploadsSuccessful;
const numFailed: number = event.data.numUploadsFailed;
const runtimeError: string = event.data.runtimeError;
const uploadDetails: UploadDetails = event.data.uploadDetails;
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
documentUploader.terminate();
if (!!runtimeError) {
reject(runtimeError);
} else if (numSuccessful === 0) {
// all uploads failed
NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`);
} else if (numFailed > 0) {
NotificationConsoleUtils.logConsoleError(
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
);
} else {
NotificationConsoleUtils.logConsoleInfo(
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
);
}
this._logUploadDetailsInConsole(uploadDetails);
resolve(uploadDetails);
};
function onerror(reject: (reason: any) => void, event: ErrorEvent) {
documentUploader.terminate();
reject(event.error);
}
const uploaderMessage: StartUploadMessageParams = {
files: fileList,
documentClientParams: {
databaseId: this.databaseId,
containerId: this.id(),
masterKey: userContext.masterKey,
endpoint: userContext.endpoint,
accessToken: userContext.accessToken,
platform: configContext.platform,
databaseAccount: userContext.databaseAccount,
},
};
return new Promise<UploadDetails>((resolve, reject) => {
documentUploader.onmessage = onmessage.bind(null, resolve, reject);
documentUploader.onerror = onerror.bind(null, reject);
documentUploader.postMessage(uploaderMessage);
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Uploading and creating documents in container ${this.id()}`
);
});
};
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> { public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
if (!this.container) { if (!this.container) {
return undefined; return undefined;
@@ -1062,13 +978,13 @@ export default class Collection implements ViewModels.Collection {
} }
} }
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> { public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file))); const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
return { data }; return { data };
} }
private _uploadFile(file: File): Promise<UploadDetailsRecord> { private uploadFile(file: File): Promise<UploadDetailsRecord> {
const reader = new FileReader(); const reader = new FileReader();
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => { const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
const fileData: string = evt.target.result; const fileData: string = evt.target.result;
@@ -1079,6 +995,7 @@ export default class Collection implements ViewModels.Collection {
resolve({ resolve({
fileName: file.name, fileName: file.name,
numSucceeded: 0, numSucceeded: 0,
numThrottled: 0,
numFailed: 1, numFailed: 1,
errors: [(evt as any).error.message], errors: [(evt as any).error.message],
}); });
@@ -1096,21 +1013,47 @@ export default class Collection implements ViewModels.Collection {
fileName: fileName, fileName: fileName,
numSucceeded: 0, numSucceeded: 0,
numFailed: 0, numFailed: 0,
numThrottled: 0,
errors: [], errors: [],
}; };
try { try {
const content = JSON.parse(documentContent); const parsedContent = JSON.parse(documentContent);
if (Array.isArray(parsedContent)) {
if (Array.isArray(content)) { const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
await Promise.all( const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) =>
content.map(async (documentContent) => { parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize)
await createDocument(this, documentContent);
record.numSucceeded++;
})
); );
for (const chunk of chunkedContent) {
let retryAttempts = 0;
let chunkComplete = false;
let documentsToAttempt = chunk;
while (retryAttempts < 10 && !chunkComplete) {
const responses = await bulkCreateDocument(this, documentsToAttempt);
const attemptedDocuments = [...documentsToAttempt];
documentsToAttempt = [];
responses.forEach((response, index) => {
if (response.statusCode === 201) {
record.numSucceeded++;
} else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]);
} else {
record.numFailed++;
}
});
if (documentsToAttempt.length === 0) {
chunkComplete = true;
break;
}
logConsoleInfo(
`${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`
);
retryAttempts++;
await sleep(retryAttempts);
}
}
} else { } else {
await createDocument(this, documentContent); await createDocument(this, parsedContent);
record.numSucceeded++; record.numSucceeded++;
} }
@@ -1122,40 +1065,6 @@ export default class Collection implements ViewModels.Collection {
} }
} }
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
const numFiles: number = uploadDetailsRecords.length;
const stackTraceLimit: number = 100;
let stackTraceCount: number = 0;
let currentFileIndex = 0;
while (stackTraceCount < stackTraceLimit && currentFileIndex < numFiles) {
const errors: string[] = uploadDetailsRecords[currentFileIndex].errors;
for (let i = 0; i < errors.length; i++) {
if (stackTraceCount >= stackTraceLimit) {
break;
}
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Document creation error for container ${this.id()} - file ${
uploadDetailsRecords[currentFileIndex].fileName
}: ${errors[i]}`
);
stackTraceCount++;
}
currentFileIndex++;
}
uploadDetailsRecords.forEach((record: UploadDetailsRecord) => {
const consoleDataType: ConsoleDataType = record.numFailed > 0 ? ConsoleDataType.Error : ConsoleDataType.Info;
NotificationConsoleUtils.logConsoleMessage(
consoleDataType,
`Item creation summary for container ${this.id()} - file ${record.fileName}: ${
record.numSucceeded
} items created, ${record.numFailed} errors`
);
});
}
/** /**
* Top-level method that will open the correct tab type depending on account API * Top-level method that will open the correct tab type depending on account API
*/ */
@@ -1163,10 +1072,10 @@ export default class Collection implements ViewModels.Collection {
if (this.container.isPreferredApiTable()) { if (this.container.isPreferredApiTable()) {
this.onTableEntitiesClick(); this.onTableEntitiesClick();
return; return;
} else if (this.container.isPreferredApiCassandra()) { } else if (userContext.apiType === "Cassandra") {
this.onTableEntitiesClick(); this.onTableEntitiesClick();
return; return;
} else if (this.container.isPreferredApiGraph()) { } else if (userContext.apiType === "Gremlin") {
this.onGraphDocumentsClick(); this.onGraphDocumentsClick();
return; return;
} else if (this.container.isPreferredApiMongoDB()) { } else if (this.container.isPreferredApiMongoDB()) {
@@ -1183,9 +1092,9 @@ export default class Collection implements ViewModels.Collection {
public getLabel(): string { public getLabel(): string {
if (this.container.isPreferredApiTable()) { if (this.container.isPreferredApiTable()) {
return "Entities"; return "Entities";
} else if (this.container.isPreferredApiCassandra()) { } else if (userContext.apiType === "Cassandra") {
return "Rows"; return "Rows";
} else if (this.container.isPreferredApiGraph()) { } else if (userContext.apiType === "Gremlin") {
return "Graph"; return "Graph";
} else if (this.container.isPreferredApiMongoDB()) { } else if (this.container.isPreferredApiMongoDB()) {
return "Documents"; return "Documents";
@@ -1240,3 +1149,7 @@ export default class Collection implements ViewModels.Collection {
} }
} }
} }
function sleep(seconds: number) {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { readCollections } from "../../Common/dataAccess/readCollections"; import { readCollections } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
@@ -78,7 +79,6 @@ export default class Database implements ViewModels.Database {
rid: this.rid, rid: this.rid,
database: this, database: this,
hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`, hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`,
isActive: ko.observable(false),
onLoadStartKey: startKey, onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}; };
@@ -172,6 +172,27 @@ export default class Database implements ViewModels.Database {
public async loadCollections(): Promise<void> { public async loadCollections(): Promise<void> {
const collectionVMs: Collection[] = []; const collectionVMs: Collection[] = [];
const collections: DataModels.Collection[] = await readCollections(this.id()); const collections: DataModels.Collection[] = await readCollections(this.id());
// TODO Remove
// This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property
if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) {
collections.map((collection) => {
if (collection.shardKey) {
const shardKey = Object.keys(collection.shardKey)[0];
collection.partitionKey = {
version: undefined,
kind: "Hash",
paths: [`/"$v"/"${shardKey.split(".").join(`"/"$v"/"`)}"/"$v"`],
};
} else {
collection.partitionKey = {
paths: ["/'$v'/'_partitionKey'/'$v'"],
kind: "Hash",
version: 2,
systemKey: true,
};
}
});
}
const deltaCollections = this.getDeltaCollections(collections); const deltaCollections = this.getDeltaCollections(collections);
collections.forEach((collection: DataModels.Collection) => { collections.forEach((collection: DataModels.Collection) => {

View File

@@ -92,7 +92,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
collection: this, collection: this,
node: this, node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
isActive: ko.observable(false),
queryText: queryText, queryText: queryText,
partitionKey: collection.partitionKey, partitionKey: collection.partitionKey,
resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(), resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(),
@@ -139,7 +138,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
documentIds: ko.observableArray<DocumentId>([]), documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
title: "Items", title: "Items",
isActive: ko.observable<boolean>(false),
collection: this, collection: this,
node: this, node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`, tabPath: `${this.databaseId}>${this.id()}>Documents`,

View File

@@ -253,7 +253,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
* @param container * @param container
*/ */
private static showScriptNodes(container: Explorer): boolean { private static showScriptNodes(container: Explorer): boolean {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
} }
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode { private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
@@ -273,7 +273,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
}); });
if (!this.container.isPreferredApiCassandra() || !this.container.isServerlessEnabled()) { if (userContext.apiType !== "Cassandra" || !this.container.isServerlessEnabled()) {
children.push({ children.push({
label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection), onClick: collection.onSettingsClick.bind(collection),

View File

@@ -76,7 +76,6 @@ export default class StoredProcedure {
collection: source, collection: source,
node: source, node: source,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/sproc`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/sproc`,
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons, onUpdateTabsButtons: source.container.onUpdateTabsButtons,
}); });
@@ -123,7 +122,6 @@ export default class StoredProcedure {
this.collection.databaseId, this.collection.databaseId,
this.collection.id() this.collection.id()
)}/sprocs/${this.id()}`, )}/sprocs/${this.id()}`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}); });
@@ -138,7 +136,7 @@ export default class StoredProcedure {
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
this.container.tabsManager.removeTabByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); this.container.tabsManager.closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);
}, },
(reason) => {} (reason) => {}

View File

@@ -58,7 +58,6 @@ export default class Trigger {
collection: source, collection: source,
node: source, node: source,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`,
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons, onUpdateTabsButtons: source.container.onUpdateTabsButtons,
}); });
@@ -98,7 +97,6 @@ export default class Trigger {
this.collection.databaseId, this.collection.databaseId,
this.collection.id() this.collection.id()
)}/triggers/${this.id()}`, )}/triggers/${this.id()}`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}); });
@@ -113,7 +111,7 @@ export default class Trigger {
deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
this.container.tabsManager.removeTabByComparator((tab) => tab.node && tab.node.rid === this.rid); this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);
}, },
(reason) => {} (reason) => {}

View File

@@ -44,7 +44,6 @@ export default class UserDefinedFunction {
collection: source, collection: source,
node: source, node: source,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`,
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons, onUpdateTabsButtons: source.container.onUpdateTabsButtons,
}); });
@@ -82,7 +81,6 @@ export default class UserDefinedFunction {
this.collection.databaseId, this.collection.databaseId,
this.collection.id() this.collection.id()
)}/udfs/${this.id()}`, )}/udfs/${this.id()}`,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}); });
@@ -106,7 +104,7 @@ export default class UserDefinedFunction {
deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
this.container.tabsManager.removeTabByComparator((tab) => tab.node && tab.node.rid === this.rid); this.container.tabsManager.closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);
}, },
(reason) => {} (reason) => {}

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