mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 17:30:46 +00:00
resolve master merge conflict
This commit is contained in:
@@ -45,7 +45,6 @@ src/Definitions/jquery.d.ts
|
||||
src/Definitions/plotly.js-cartesian-dist.d-min.ts
|
||||
src/Definitions/png.d.ts
|
||||
src/Definitions/svg.d.ts
|
||||
src/Definitions/worker.d.ts
|
||||
src/Explorer/ComponentRegisterer.test.ts
|
||||
src/Explorer/ComponentRegisterer.ts
|
||||
src/Explorer/ContextMenuButtonFactory.ts
|
||||
@@ -118,8 +117,6 @@ src/Explorer/Panes/AddCollectionPane.ts
|
||||
src/Explorer/Panes/BrowseQueriesPane.ts
|
||||
src/Explorer/Panes/CassandraAddCollectionPane.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.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/EditTableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
|
||||
src/Explorer/Panes/Tables/QuerySelectPane.ts
|
||||
src/Explorer/Panes/Tables/TableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
|
||||
@@ -248,7 +244,6 @@ src/Utils/QueryUtils.test.ts
|
||||
src/applyExplorerBindings.ts
|
||||
src/global.d.ts
|
||||
src/setupTests.ts
|
||||
src/workers/upload/index.ts
|
||||
src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx
|
||||
src/Explorer/Controls/Accordion/AccordionComponent.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/Menus/CommandBar/CommandBarUtil.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/NotebookComponentAdapter.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
browser: 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"],
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
@@ -20,7 +20,7 @@ module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.tsx"],
|
||||
extends: ["plugin:react/recommended"], // TODO: Add react-hooks
|
||||
extends: ["plugin:react/recommended"],
|
||||
plugins: ["react"],
|
||||
},
|
||||
{
|
||||
@@ -42,6 +42,8 @@ module.exports = {
|
||||
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
|
||||
eqeqeq: "error",
|
||||
"react/display-name": "off",
|
||||
"react-hooks/rules-of-hooks": "warn", // TODO: error
|
||||
"react-hooks/exhaustive-deps": "warn", // TODO: error
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
@@ -69,7 +69,6 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
"^.*[.](svg|png|gif|less|css)$": "<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
|
||||
"^dnd-core$": "dnd-core/dist/cjs",
|
||||
"^react-dnd$": "react-dnd/dist/cjs",
|
||||
|
||||
1042
package-lock.json
generated
1042
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.9.0",
|
||||
"@azure/cosmos": "3.10.5",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.2.1",
|
||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@nteract/iron-icons": "1.0.0",
|
||||
"@nteract/jupyter-widgets": "2.0.0",
|
||||
"@nteract/logos": "1.0.0",
|
||||
"@nteract/markdown": "4.4.0",
|
||||
"@nteract/markdown": "4.6.0",
|
||||
"@nteract/monaco-editor": "3.2.2",
|
||||
"@nteract/octicons": "2.0.0",
|
||||
"@nteract/outputs": "3.0.9",
|
||||
@@ -94,6 +94,7 @@
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"rxjs": "6.6.3",
|
||||
"sanitize-html": "2.3.3",
|
||||
"styled-components": "4.3.2",
|
||||
"swr": "0.4.0",
|
||||
"terser-webpack-plugin": "3.1.0",
|
||||
@@ -122,10 +123,11 @@
|
||||
"@types/prop-types": "15.5.8",
|
||||
"@types/puppeteer": "5.4.3",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/react": "17.0.3",
|
||||
"@types/react-dom": "17.0.3",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
"@types/sanitize-html": "1.27.2",
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@types/underscore": "1.7.36",
|
||||
@@ -178,8 +180,7 @@
|
||||
"webpack": "4.43.0",
|
||||
"webpack-bundle-analyzer": "3.6.1",
|
||||
"webpack-cli": "3.3.10",
|
||||
"webpack-dev-server": "3.11.0",
|
||||
"worker-loader": "2.0.0"
|
||||
"webpack-dev-server": "3.11.0"
|
||||
},
|
||||
"scripts": {
|
||||
"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}\"",
|
||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||
"build:contracts": "npm run compile:contracts",
|
||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||
"autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",
|
||||
"strict:find": "node ./strict-null-checks/find.js",
|
||||
"strict:add": "node ./strict-null-checks/auto-add.js",
|
||||
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
|
||||
"generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require("express");
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
const port = process.env.PORT || 3000;
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const api = createProxyMiddleware("/api", {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
@@ -39,6 +40,29 @@ const app = express();
|
||||
app.use(api);
|
||||
app.use(proxy);
|
||||
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, () => {
|
||||
console.log(`Example app listening on port: ${port}`);
|
||||
});
|
||||
|
||||
659
preview/package-lock.json
generated
659
preview/package-lock.json
generated
@@ -1,8 +1,658 @@
|
||||
{
|
||||
"name": "preview",
|
||||
"name": "cosmos-explorer-preview",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 2,
|
||||
"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": {
|
||||
"@types/http-proxy": {
|
||||
"version": "1.17.5",
|
||||
@@ -334,6 +984,11 @@
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"http-proxy-middleware": "^1.1.0"
|
||||
"http-proxy-middleware": "^1.1.0",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
||||
}
|
||||
}
|
||||
|
||||
let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
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
|
||||
key: userContext.masterKey,
|
||||
@@ -89,5 +92,6 @@ export function client(): Cosmos.CosmosClient {
|
||||
if (configContext.PROXY_PATH !== undefined) {
|
||||
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
|
||||
}
|
||||
return new Cosmos.CosmosClient(options);
|
||||
_client = new Cosmos.CosmosClient(options);
|
||||
return _client;
|
||||
}
|
||||
|
||||
@@ -48,32 +48,18 @@ export function sendCachedDataMessage<TResponseDataModel>(
|
||||
}
|
||||
|
||||
export function sendMessage(data: any): void {
|
||||
if (canSendMessage()) {
|
||||
// We try to find data explorer window first, then fallback to current window
|
||||
const portalChildWindow = getDataExplorerWindow(window) || window;
|
||||
portalChildWindow.parent.postMessage(
|
||||
{
|
||||
_sendMessage({
|
||||
signature: "pcIframe",
|
||||
data: data,
|
||||
},
|
||||
portalChildWindow.document.referrer || "*"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function sendReadyMessage(): void {
|
||||
if (canSendMessage()) {
|
||||
// We try to find data explorer window first, then fallback to current window
|
||||
const portalChildWindow = getDataExplorerWindow(window) || window;
|
||||
portalChildWindow.parent.postMessage(
|
||||
{
|
||||
_sendMessage({
|
||||
signature: "pcIframe",
|
||||
kind: "ready",
|
||||
data: "ready",
|
||||
},
|
||||
portalChildWindow.document.referrer || "*"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 || "*");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
39
src/Common/dataAccess/bulkCreateDocument.ts
Normal file
39
src/Common/dataAccess/bulkCreateDocument.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { userContext } from "../../UserContext";
|
||||
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 { 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 { 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[]> {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||
@@ -17,7 +17,6 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readCollectionsWithARM(databaseId);
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
import { ContainerDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { ContainerDefinition } from "@azure/cosmos";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import {
|
||||
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 { userContext } from "../../UserContext";
|
||||
import {
|
||||
createUpdateCassandraTable,
|
||||
getCassandraTable,
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import {
|
||||
createUpdateMongoDBCollection,
|
||||
getMongoDBCollection,
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import {
|
||||
createUpdateGremlinGraph,
|
||||
getGremlinGraph,
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import {
|
||||
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 { handleError } from "../ErrorHandlingUtils";
|
||||
import {
|
||||
ExtendedResourceProperties,
|
||||
MongoDBCollectionCreateUpdateParameters,
|
||||
SqlContainerCreateUpdateParameters,
|
||||
SqlContainerResource,
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function updateCollection(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
newCollection: Collection,
|
||||
newCollection: Partial<Collection>,
|
||||
options: RequestOptions = {}
|
||||
): Promise<Collection> {
|
||||
let collection: Collection;
|
||||
@@ -43,7 +41,6 @@ export async function updateCollection(
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||
@@ -69,7 +66,7 @@ export async function updateCollection(
|
||||
async function updateCollectionWithARM(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
newCollection: Collection
|
||||
newCollection: Partial<Collection>
|
||||
): Promise<Collection> {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
@@ -85,6 +82,15 @@ async function updateCollectionWithARM(
|
||||
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||
case DefaultAccountExperienceType.Table:
|
||||
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
return updateMongoDBCollection(
|
||||
databaseId,
|
||||
collectionId,
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
newCollection
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
@@ -96,7 +102,7 @@ async function updateSqlContainer(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
newCollection: Collection
|
||||
newCollection: Partial<Collection>
|
||||
): Promise<Collection> {
|
||||
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||
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}`);
|
||||
}
|
||||
|
||||
export async function updateMongoDBCollectionThroughRP(
|
||||
export async function updateMongoDBCollection(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
newCollection: MongoDBCollectionResource,
|
||||
updateOptions?: CreateUpdateOptions
|
||||
): Promise<MongoDBCollectionResource> {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
newCollection: Partial<Collection>
|
||||
): Promise<Collection> {
|
||||
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||
const updateParams: MongoDBCollectionCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: newCollection,
|
||||
options: updateOptions,
|
||||
},
|
||||
};
|
||||
|
||||
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||
const updateResponse = await createUpdateMongoDBCollection(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId,
|
||||
updateParams
|
||||
getResponse as MongoDBCollectionCreateUpdateParameters
|
||||
);
|
||||
|
||||
return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource);
|
||||
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
@@ -157,7 +154,7 @@ async function updateCassandraTable(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
newCollection: Collection
|
||||
newCollection: Partial<Collection>
|
||||
): Promise<Collection> {
|
||||
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||
@@ -184,7 +181,7 @@ async function updateGremlinGraph(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
newCollection: Collection
|
||||
newCollection: Partial<Collection>
|
||||
): Promise<Collection> {
|
||||
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||
@@ -208,7 +205,7 @@ async function updateTable(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
newCollection: Collection
|
||||
newCollection: Partial<Collection>
|
||||
): Promise<Collection> {
|
||||
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||
|
||||
@@ -121,6 +121,10 @@ export interface ISchemaRequest {
|
||||
}
|
||||
|
||||
export interface Collection extends Resource {
|
||||
// Only in Mongo collections loaded via ARM
|
||||
shardKey?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
defaultTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
partitionKey?: PartitionKey;
|
||||
|
||||
@@ -15,7 +15,6 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import Trigger from "../Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import * as DataModels from "./DataModels";
|
||||
import { SubscriptionType } from "./SubscriptionType";
|
||||
|
||||
@@ -23,6 +22,14 @@ export interface TokenProvider {
|
||||
getAuthHeader(): Promise<Headers>;
|
||||
}
|
||||
|
||||
export interface UploadDetailsRecord {
|
||||
fileName: string;
|
||||
numSucceeded: number;
|
||||
numFailed: number;
|
||||
numThrottled: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface QueryResultsMetadata {
|
||||
hasMoreResults: boolean;
|
||||
firstItemIndex: number;
|
||||
@@ -174,7 +181,7 @@ export interface Collection extends CollectionBase {
|
||||
|
||||
onDragOver(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;
|
||||
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||
@@ -269,7 +276,6 @@ export interface TabOptions {
|
||||
tabKind: CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
isActive: ko.Observable<boolean>;
|
||||
hashLocation: string;
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
|
||||
isTabsContentExpanded?: ko.Observable<boolean>;
|
||||
@@ -390,6 +396,9 @@ export interface DataExplorerInputsFrame {
|
||||
dataExplorerVersion?: string;
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
flights?: readonly string[];
|
||||
features?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SelfServeFrameInputs {
|
||||
|
||||
7
src/Definitions/worker.d.ts
vendored
7
src/Definitions/worker.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
declare module "worker-loader!*" {
|
||||
class WebpackWorker extends Worker {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export default WebpackWorker;
|
||||
}
|
||||
@@ -73,10 +73,6 @@ describe("Component Registerer", () => {
|
||||
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", () => {
|
||||
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -57,16 +57,11 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
|
||||
|
||||
// Panes
|
||||
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-styling-pane", new PaneComponents.GraphStylingPaneComponent());
|
||||
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
|
||||
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("string-input-pane", new PaneComponents.StringInputPaneComponent());
|
||||
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
selectedCollection: ViewModels.Collection
|
||||
): TreeNodeMenuItem[] {
|
||||
const items: TreeNodeMenuItem[] = [];
|
||||
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
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({
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
onClick: () => {
|
||||
@@ -123,7 +123,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
container: Explorer,
|
||||
storedProcedure: StoredProcedure
|
||||
): TreeNodeMenuItem[] {
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
}
|
||||
|
||||
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
container: Explorer,
|
||||
userDefinedFunction: UserDefinedFunction
|
||||
): TreeNodeMenuItem[] {
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -21,18 +21,18 @@ import {
|
||||
Text,
|
||||
} from "office-ui-fabric-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 { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { Dialog, DialogProps } from "../Dialog";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
import "./GalleryViewerComponent.less";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
||||
import "./GalleryViewerComponent.less";
|
||||
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 {
|
||||
container?: Explorer;
|
||||
@@ -138,11 +138,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
key: SortBy.MostRecent,
|
||||
text: GalleryViewerComponent.mostRecentText,
|
||||
},
|
||||
];
|
||||
this.sortingOptions.push({
|
||||
{
|
||||
key: SortBy.MostFavorited,
|
||||
text: GalleryViewerComponent.mostFavoritedText,
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
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
|
||||
@@ -654,7 +654,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
};
|
||||
|
||||
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 = {
|
||||
data,
|
||||
isFavorite,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { shallow } from "enzyme";
|
||||
import ko from "knockout";
|
||||
import React from "react";
|
||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
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 { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
|
||||
@@ -23,13 +23,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
changeFeedPolicy: undefined,
|
||||
analyticalStorageTtl: undefined,
|
||||
geospatialConfig: undefined,
|
||||
} as DataModels.Collection),
|
||||
updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({
|
||||
id: undefined,
|
||||
shardKey: undefined,
|
||||
indexes: [],
|
||||
analyticalStorageTtl: undefined,
|
||||
} as MongoDBCollectionResource),
|
||||
}),
|
||||
}));
|
||||
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
|
||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
|
||||
@@ -44,7 +39,6 @@ describe("SettingsComponent", () => {
|
||||
tabPath: "",
|
||||
node: undefined,
|
||||
hashLocation: "settings",
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: undefined,
|
||||
}),
|
||||
};
|
||||
@@ -113,7 +107,13 @@ describe("SettingsComponent", () => {
|
||||
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
|
||||
|
||||
const newContainer = new Explorer();
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableCassandra" }],
|
||||
},
|
||||
} as DataModels.DatabaseAccount,
|
||||
});
|
||||
|
||||
const newCollection = { ...collection };
|
||||
newCollection.container = newContainer;
|
||||
@@ -193,7 +193,6 @@ describe("SettingsComponent", () => {
|
||||
};
|
||||
await settingsComponentInstance.onSaveClick();
|
||||
expect(updateCollection).toBeCalled();
|
||||
expect(updateMongoDBCollectionThroughRP).toBeCalled();
|
||||
expect(updateOffer).toBeCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AuthType } from "../../../AuthType";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||
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 { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
@@ -137,7 +137,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.offer = this.collection?.offer();
|
||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||
this.shouldShowIndexingPolicyEditor =
|
||||
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
|
||||
this.container && userContext.apiType !== "Cassandra" && !this.container.isPreferredApiMongoDB();
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
|
||||
@@ -299,7 +299,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
|
||||
|
||||
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
|
||||
this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection);
|
||||
this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
public hasConflictResolution = (): boolean =>
|
||||
this.container?.databaseAccount &&
|
||||
@@ -782,12 +782,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
|
||||
try {
|
||||
const newMongoIndexes = this.getMongoIndexesToSave();
|
||||
const newMongoCollection: MongoDBCollectionResource = {
|
||||
const newMongoCollection = {
|
||||
...this.mongoDBCollectionResource,
|
||||
indexes: newMongoIndexes,
|
||||
};
|
||||
|
||||
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
|
||||
this.mongoDBCollectionResource = await updateCollection(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
newMongoCollection
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
|
||||
import { container, collection } from "../TestUtils";
|
||||
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
|
||||
import ko from "knockout";
|
||||
import { DatabaseAccount } from "../../../../Contracts/DataModels";
|
||||
import { updateUserContext } from "../../../../UserContext";
|
||||
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", () => {
|
||||
container.isPreferredApiDocumentDB = ko.computed(() => true);
|
||||
|
||||
const baseProps: SubSettingsComponentProps = {
|
||||
collection: collection,
|
||||
container: container,
|
||||
@@ -106,8 +105,13 @@ describe("SubSettingsComponent", () => {
|
||||
|
||||
it("partitionKey not visible", () => {
|
||||
const newContainer = new Explorer();
|
||||
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableCassandra" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const props = { ...baseProps, container: newContainer };
|
||||
const subSettingsComponent = new SubSettingsComponent(props);
|
||||
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false);
|
||||
|
||||
@@ -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 ViewModels from "../../../../Contracts/ViewModels";
|
||||
import {
|
||||
GeospatialConfigType,
|
||||
TtlType,
|
||||
ChangeFeedPolicyState,
|
||||
isDirty,
|
||||
IsComponentDirtyResult,
|
||||
TtlOn,
|
||||
TtlOff,
|
||||
TtlOnNoDefault,
|
||||
getSanitizedInputValue,
|
||||
} from "../SettingsUtils";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
changeFeedPolicyToolTip,
|
||||
getChoiceGroupStyles,
|
||||
getTextFieldStyles,
|
||||
messageBarStyles,
|
||||
subComponentStackProps,
|
||||
titleAndInputStackProps,
|
||||
getChoiceGroupStyles,
|
||||
ttlWarning,
|
||||
messageBarStyles,
|
||||
} from "../SettingsRenderUtils";
|
||||
import {
|
||||
ChangeFeedPolicyState,
|
||||
GeospatialConfigType,
|
||||
getSanitizedInputValue,
|
||||
IsComponentDirtyResult,
|
||||
isDirty,
|
||||
TtlOff,
|
||||
TtlOn,
|
||||
TtlOnNoDefault,
|
||||
TtlType,
|
||||
} from "../SettingsUtils";
|
||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
|
||||
export interface SubSettingsComponentProps {
|
||||
@@ -60,17 +70,15 @@ export interface SubSettingsComponentProps {
|
||||
|
||||
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private ttlVisible: boolean;
|
||||
private geospatialVisible: boolean;
|
||||
private partitionKeyValue: string;
|
||||
private partitionKeyName: string;
|
||||
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false;
|
||||
this.geospatialVisible = this.props.container.isPreferredApiDocumentDB();
|
||||
this.geospatialVisible = userContext.apiType === "SQL";
|
||||
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 {
|
||||
@@ -170,7 +178,19 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
): void =>
|
||||
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
|
||||
|
||||
private getTtlComponent = (): JSX.Element => (
|
||||
private getTtlComponent = (): JSX.Element =>
|
||||
userContext.apiType === "Mongo" ? (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={{ text: { fontSize: 14 } }}
|
||||
>
|
||||
To enable time-to-live (TTL) for your collection/documents,
|
||||
<Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank">
|
||||
create a TTL index
|
||||
</Link>
|
||||
.
|
||||
</MessageBar>
|
||||
) : (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="timeToLive"
|
||||
@@ -300,7 +320,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
|
||||
public getPartitionKeyVisible = (): boolean => {
|
||||
if (
|
||||
this.props.container.isPreferredApiCassandra() ||
|
||||
userContext.apiType === "Cassandra" ||
|
||||
this.props.container.isPreferredApiTable() ||
|
||||
!this.props.collection.partitionKeyProperty ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
|
||||
@@ -315,7 +335,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.ttlVisible && this.getTtlComponent()}
|
||||
{userContext.apiType !== "Cassandra" && this.getTtlComponent()}
|
||||
|
||||
{this.geospatialVisible && this.getGeoSpatialComponent()}
|
||||
|
||||
|
||||
@@ -115,21 +115,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -220,27 +205,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -531,21 +495,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -614,9 +563,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -656,27 +602,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -868,21 +793,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -973,27 +883,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -1284,21 +1173,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -1367,9 +1241,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -1409,27 +1280,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -1634,21 +1484,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -1739,27 +1574,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -2050,21 +1864,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -2133,9 +1932,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -2175,27 +1971,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -2387,21 +2162,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -2492,27 +2252,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -2803,21 +2542,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -2886,9 +2610,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -2928,27 +2649,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
|
||||
@@ -4,6 +4,7 @@ jest.mock("../../Common/dataAccess/createDocument");
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
@@ -13,11 +14,8 @@ describe("ContainerSampleGenerator", () => {
|
||||
const createExplorerStub = (database: ViewModels.Database): Explorer => {
|
||||
const explorerStub = {} as Explorer;
|
||||
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
|
||||
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false);
|
||||
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
|
||||
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
|
||||
explorerStub.findDatabaseWithId = () => database;
|
||||
explorerStub.refreshAllDatabases = () => Q.resolve();
|
||||
@@ -31,7 +29,7 @@ describe("ContainerSampleGenerator", () => {
|
||||
it("should insert documents for sql API account", async () => {
|
||||
const sampleCollectionId = "SampleCollection";
|
||||
const sampleDatabaseId = "SampleDB";
|
||||
|
||||
updateUserContext({});
|
||||
const sampleData = {
|
||||
databaseId: sampleDatabaseId,
|
||||
offerThroughput: 400,
|
||||
@@ -66,7 +64,7 @@ describe("ContainerSampleGenerator", () => {
|
||||
database.findCollectionWithId = () => collection;
|
||||
|
||||
const explorerStub = createExplorerStub(database);
|
||||
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true);
|
||||
|
||||
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
|
||||
generator.setData(sampleData);
|
||||
|
||||
@@ -116,7 +114,13 @@ describe("ContainerSampleGenerator", () => {
|
||||
collection.databaseId = database.id();
|
||||
|
||||
const explorerStub = createExplorerStub(database);
|
||||
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableGremlin" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
|
||||
generator.setData(sampleData);
|
||||
@@ -125,31 +129,45 @@ describe("ContainerSampleGenerator", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => true);
|
||||
explorerStub.defaultExperience = ko.observable<string>(experience);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableMongo" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
// Rejects with error that contains experience
|
||||
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||
});
|
||||
|
||||
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);
|
||||
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => true);
|
||||
explorerStub.defaultExperience = ko.observable<string>(experience);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
// Rejects with error that contains experience
|
||||
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||
});
|
||||
|
||||
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);
|
||||
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => true);
|
||||
explorerStub.defaultExperience = ko.observable<string>(experience);
|
||||
|
||||
// Rejects with error that contains experience
|
||||
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||
});
|
||||
|
||||
@@ -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 { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
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 {
|
||||
data: any[];
|
||||
@@ -23,16 +23,16 @@ export class ContainerSampleGenerator {
|
||||
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
|
||||
const generator = new ContainerSampleGenerator(container);
|
||||
let dataFileContent: any;
|
||||
if (container.isPreferredApiGraph()) {
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
dataFileContent = await import(
|
||||
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
|
||||
);
|
||||
} else if (container.isPreferredApiDocumentDB()) {
|
||||
} else if (userContext.apiType === "SQL") {
|
||||
dataFileContent = await import(
|
||||
/* webpackChunkName: "sqlSampleJsonData" */ "../../../sampleData/sqlSampleData.json"
|
||||
);
|
||||
} 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);
|
||||
@@ -73,7 +73,7 @@ export class ContainerSampleGenerator {
|
||||
}
|
||||
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
|
||||
// (e.g. adding edge requires vertices to be present)
|
||||
const queries: string[] = this.sampleDataFile.data;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
@@ -56,6 +57,6 @@ export class DataSamplesUtil {
|
||||
}
|
||||
|
||||
public isSampleContainerCreationSupported(): boolean {
|
||||
return this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph();
|
||||
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import * as PricingUtils from "../Utils/PricingUtils";
|
||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -48,11 +49,10 @@ import { NotebookContentItem, NotebookContentItemType } from "./Notebook/Noteboo
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import AddCollectionPane from "./Panes/AddCollectionPane";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { AddDatabasePane } from "./Panes/AddDatabasePane";
|
||||
import AddDatabasePane from "./Panes/AddDatabasePane";
|
||||
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel";
|
||||
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
|
||||
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
|
||||
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
|
||||
@@ -65,9 +65,10 @@ import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
|
||||
import { StringInputPane } from "./Panes/StringInputPane";
|
||||
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
|
||||
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
||||
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
import TabsBase from "./Tabs/TabsBase";
|
||||
@@ -78,8 +79,6 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
||||
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import Trigger from "./Tree/Trigger";
|
||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@@ -93,6 +92,7 @@ export interface ExplorerParams {
|
||||
closeSidePanel: () => void;
|
||||
closeDialog: () => void;
|
||||
openDialog: (props: DialogProps) => void;
|
||||
tabsManager: TabsManager;
|
||||
}
|
||||
|
||||
export default class Explorer {
|
||||
@@ -116,26 +116,12 @@ export default class Explorer {
|
||||
* Use userContext.apiType instead
|
||||
* */
|
||||
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
|
||||
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
|
||||
* */
|
||||
public isPreferredApiMongoDB: ko.Computed<boolean>;
|
||||
/**
|
||||
* @deprecated
|
||||
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
|
||||
* */
|
||||
public isPreferredApiGraph: ko.Computed<boolean>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
|
||||
@@ -188,12 +174,11 @@ export default class Explorer {
|
||||
public tabsManager: TabsManager;
|
||||
|
||||
// Contextual panes
|
||||
public addDatabasePane: AddDatabasePane;
|
||||
public addCollectionPane: AddCollectionPane;
|
||||
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
|
||||
public graphStylingPane: GraphStylingPane;
|
||||
public addTableEntityPane: AddTableEntityPane;
|
||||
public editTableEntityPane: EditTableEntityPane;
|
||||
public querySelectPane: QuerySelectPane;
|
||||
public newVertexPane: NewVertexPane;
|
||||
public cassandraAddCollectionPane: CassandraAddCollectionPane;
|
||||
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(() => {
|
||||
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
|
||||
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase();
|
||||
@@ -497,7 +468,9 @@ export default class Explorer {
|
||||
|
||||
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.selectedDatabaseId = ko.computed<string>(() => {
|
||||
@@ -521,16 +494,16 @@ export default class Explorer {
|
||||
}
|
||||
});
|
||||
|
||||
this.addCollectionPane = new AddCollectionPane({
|
||||
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
|
||||
id: "addcollectionpane",
|
||||
this.addDatabasePane = new AddDatabasePane({
|
||||
id: "adddatabasepane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({
|
||||
id: "deletecollectionconfirmationpane",
|
||||
this.addCollectionPane = new AddCollectionPane({
|
||||
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
|
||||
id: "addcollectionpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
@@ -557,13 +530,6 @@ export default class Explorer {
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.querySelectPane = new QuerySelectPane({
|
||||
id: "queryselectpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.newVertexPane = new NewVertexPane({
|
||||
id: "newvertexpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
@@ -592,21 +558,26 @@ export default class Explorer {
|
||||
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.addDatabasePane,
|
||||
this.addCollectionPane,
|
||||
this.deleteCollectionConfirmationPane,
|
||||
this.graphStylingPane,
|
||||
this.addTableEntityPane,
|
||||
this.editTableEntityPane,
|
||||
this.querySelectPane,
|
||||
this.newVertexPane,
|
||||
this.cassandraAddCollectionPane,
|
||||
this.stringInputPane,
|
||||
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);
|
||||
|
||||
document.addEventListener(
|
||||
@@ -634,8 +605,6 @@ export default class Explorer {
|
||||
this.addCollectionPane.collectionWithThroughputInSharedTitle(
|
||||
"Provision dedicated throughput for this container"
|
||||
);
|
||||
this.deleteCollectionConfirmationPane.title("Delete Container");
|
||||
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id");
|
||||
this.refreshTreeTitle("Refresh containers");
|
||||
break;
|
||||
case "Mongo":
|
||||
@@ -662,8 +631,6 @@ export default class Explorer {
|
||||
this.addCollectionPane.title("Add Graph");
|
||||
this.addCollectionPane.collectionIdTitle("Graph id");
|
||||
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");
|
||||
break;
|
||||
case "Tables":
|
||||
@@ -679,8 +646,6 @@ export default class Explorer {
|
||||
this.refreshTreeTitle("Refresh tables");
|
||||
this.addTableEntityPane.title("Add 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();
|
||||
break;
|
||||
case "Cassandra":
|
||||
@@ -696,8 +661,6 @@ export default class Explorer {
|
||||
this.refreshTreeTitle("Refresh tables");
|
||||
this.addTableEntityPane.title("Add 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();
|
||||
break;
|
||||
}
|
||||
@@ -915,10 +878,8 @@ export default class Explorer {
|
||||
|
||||
// TODO: Refactor
|
||||
const deferred: Q.Deferred<any> = Q.defer();
|
||||
this._setLoadingStatusText("Fetching databases...");
|
||||
readDatabases().then(
|
||||
(databases: DataModels.Database[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched databases.");
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
@@ -931,20 +892,16 @@ export default class Explorer {
|
||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
||||
this.selectedNode(currentlySelectedNode);
|
||||
this._setLoadingStatusText("Fetching containers...");
|
||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
|
||||
() => {
|
||||
this._setLoadingStatusText("Successfully fetched containers.");
|
||||
deferred.resolve();
|
||||
},
|
||||
(reason) => {
|
||||
this._setLoadingStatusText("Failed to fetch containers.");
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this._setLoadingStatusText("Failed to fetch databases.");
|
||||
deferred.reject(error);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
@@ -1020,7 +977,7 @@ export default class Explorer {
|
||||
|
||||
// Facade
|
||||
public provideFeedbackEmail = () => {
|
||||
window.open(Constants.Urls.feedbackEmail, "_self");
|
||||
window.open(Constants.Urls.feedbackEmail, "_blank");
|
||||
};
|
||||
|
||||
public async getArcadiaToken(): Promise<string> {
|
||||
@@ -1291,49 +1248,6 @@ export default class Explorer {
|
||||
: 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 {
|
||||
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
|
||||
}
|
||||
@@ -1662,7 +1576,6 @@ export default class Explorer {
|
||||
collection: null,
|
||||
masterKey: userContext.masterKey || "",
|
||||
hashLocation: "notebooks",
|
||||
isActive: ko.observable(false),
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
@@ -2068,7 +1981,6 @@ export default class Explorer {
|
||||
tabPath: title,
|
||||
collection: null,
|
||||
hashLocation: hashLocation,
|
||||
isActive: ko.observable(false),
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
@@ -2173,7 +2085,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public onNewCollectionClicked(): void {
|
||||
if (this.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
this.cassandraAddCollectionPane.open();
|
||||
} else if (userContext.features.enableReactPane) {
|
||||
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 {
|
||||
const title = "Enable Notebooks (Preview)";
|
||||
const description =
|
||||
@@ -2309,14 +2195,13 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public openDeleteCollectionConfirmationPane(): void {
|
||||
userContext.features.enableKOPanel
|
||||
? this.deleteCollectionConfirmationPane.open()
|
||||
: this.openSidePanel(
|
||||
"Delete Collection",
|
||||
let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience);
|
||||
this.openSidePanel(
|
||||
"Delete " + collectionName,
|
||||
<DeleteCollectionConfirmationPanel
|
||||
explorer={this}
|
||||
closePanel={() => this.closeSidePanel()}
|
||||
openNotificationConsole={() => this.expandConsole()}
|
||||
collectionName={collectionName}
|
||||
closePanel={this.closeSidePanel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2341,10 +2226,14 @@ export default class Explorer {
|
||||
this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />);
|
||||
}
|
||||
|
||||
public openExecuteSprocParamsPanel(): void {
|
||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||
this.openSidePanel(
|
||||
"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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NeighborVertexBasicInfo } from "./GraphExplorer";
|
||||
import * as GraphData from "./GraphData";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import * as GraphData from "./GraphData";
|
||||
import { NeighborVertexBasicInfo } from "./GraphExplorer";
|
||||
|
||||
interface JoinArrayMaxCharOutput {
|
||||
result: string; // string output
|
||||
@@ -13,9 +13,9 @@ interface EdgePropertyType {
|
||||
inV?: string;
|
||||
}
|
||||
|
||||
export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
|
||||
export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => {
|
||||
return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect all edges from this node
|
||||
@@ -23,11 +23,11 @@ export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
|
||||
* @param graphData
|
||||
* @param newNodes (optional) object describing new nodes encountered
|
||||
*/
|
||||
export function createEdgesfromNode(
|
||||
export const createEdgesfromNode = (
|
||||
vertex: GraphData.GremlinVertex,
|
||||
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
|
||||
newNodes?: { [id: string]: boolean }
|
||||
): void {
|
||||
): void => {
|
||||
if (Object.prototype.hasOwnProperty.call(vertex, "outE")) {
|
||||
const outE = vertex.outE;
|
||||
for (const label in outE) {
|
||||
@@ -66,7 +66,7 @@ export function createEdgesfromNode(
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'".
|
||||
@@ -75,7 +75,7 @@ export function createEdgesfromNode(
|
||||
* @param maxSize
|
||||
* @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) {
|
||||
return { result: "", consumedCount: 0 };
|
||||
}
|
||||
@@ -96,16 +96,16 @@ export function getLimitedArrayString(array: string[], maxSize: number): JoinArr
|
||||
result: output,
|
||||
consumedCount: i + 1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createFetchEdgePairQuery(
|
||||
export const createFetchEdgePairQuery = (
|
||||
outE: boolean,
|
||||
pkid: string,
|
||||
excludedEdgeIds: string[],
|
||||
startIndex: number,
|
||||
pageSize: number,
|
||||
withoutStepArgMaxLenght: number
|
||||
): string {
|
||||
): string => {
|
||||
let gremlinQuery: string;
|
||||
if (excludedEdgeIds.length > 0) {
|
||||
// build a string up to max char
|
||||
@@ -128,15 +128,15 @@ export function createFetchEdgePairQuery(
|
||||
}().as('v').select('e', 'v')`;
|
||||
}
|
||||
return gremlinQuery;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Trim graph
|
||||
*/
|
||||
export function trimGraph(
|
||||
export const trimGraph = (
|
||||
currentRoot: GraphData.GremlinVertex,
|
||||
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
|
||||
) {
|
||||
): void => {
|
||||
const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId);
|
||||
graphData.unloadAllVertices(importantNodes);
|
||||
|
||||
@@ -144,32 +144,32 @@ export function trimGraph(
|
||||
$.each(graphData.ids, (index: number, id: string) => {
|
||||
graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function addRootChildToGraph(
|
||||
export const addRootChildToGraph = (
|
||||
root: GraphData.GremlinVertex,
|
||||
child: GraphData.GremlinVertex,
|
||||
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
|
||||
) {
|
||||
): void => {
|
||||
child._ancestorsId = (root._ancestorsId || []).concat([root.id]);
|
||||
graphData.addVertex(child);
|
||||
createEdgesfromNode(child, graphData);
|
||||
graphData.addNeighborInfo(child);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now.
|
||||
* @param value
|
||||
*/
|
||||
export function escapeDoubleQuotes(value: string): string {
|
||||
export const escapeDoubleQuotes = (value: string): string => {
|
||||
return value === undefined ? value : value.replace(/"/g, '\\"');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Surround with double-quotes if val is a string.
|
||||
* @param val
|
||||
*/
|
||||
export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
|
||||
export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string => {
|
||||
switch (ip.type) {
|
||||
case "number":
|
||||
case "boolean":
|
||||
@@ -179,12 +179,12 @@ export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
|
||||
default:
|
||||
return `"${escapeDoubleQuotes(ip.value as string)}"`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now.
|
||||
* @param value
|
||||
*/
|
||||
export function escapeSingleQuotes(value: string): string {
|
||||
export const escapeSingleQuotes = (value: string): string => {
|
||||
return value === undefined ? value : value.replace(/'/g, "\\'");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as ko from "knockout";
|
||||
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import * as React from "react";
|
||||
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 * as CommandBarUtil from "./CommandBarUtil";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
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 {
|
||||
public parameters: ko.Observable<number>;
|
||||
@@ -23,17 +23,14 @@ export class CommandBarComponentAdapter implements ReactAdapter {
|
||||
constructor(container: Explorer) {
|
||||
this.container = container;
|
||||
this.tabsButtons = [];
|
||||
this.isNotebookTabActive = ko.computed(() =>
|
||||
container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
|
||||
this.isNotebookTabActive = ko.computed(
|
||||
() => 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
|
||||
const toWatch = [
|
||||
container.isPreferredApiTable,
|
||||
container.isPreferredApiMongoDB,
|
||||
container.isPreferredApiDocumentDB,
|
||||
container.isPreferredApiCassandra,
|
||||
container.isPreferredApiGraph,
|
||||
container.deleteCollectionText,
|
||||
container.deleteDatabaseText,
|
||||
container.addCollectionText,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ko from "knockout";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -17,7 +18,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
@@ -56,7 +56,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
@@ -119,7 +118,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
@@ -208,15 +206,26 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => true);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableCassandra" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
});
|
||||
|
||||
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 openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
@@ -281,7 +290,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
@@ -337,7 +345,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
@@ -347,6 +354,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
|
||||
it("should only show New SQL Query and Open Query buttons", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
kind: "DocumentDB",
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
expect(buttons.length).toBe(2);
|
||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||
|
||||
@@ -74,7 +74,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
||||
buttons.push(createOpenMongoTerminalButton(container));
|
||||
}
|
||||
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
buttons.push(createOpenCassandraTerminalButton(container));
|
||||
}
|
||||
}
|
||||
@@ -90,15 +90,15 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
||||
buttons.push(createDivider());
|
||||
}
|
||||
|
||||
const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
||||
const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
if (isSqlQuerySupported) {
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(container);
|
||||
buttons.push(newSqlQueryBtn);
|
||||
}
|
||||
|
||||
const isSupportedOpenQueryApi =
|
||||
container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph();
|
||||
const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
||||
userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin";
|
||||
const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
|
||||
const openQueryBtn = createOpenQueryButton(container);
|
||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
|
||||
@@ -107,7 +107,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
||||
buttons.push(createOpenQueryFromDiskButton(container));
|
||||
}
|
||||
|
||||
if (areScriptsSupported(container)) {
|
||||
if (areScriptsSupported()) {
|
||||
const label = "New Stored Procedure";
|
||||
const newStoredProcedureBtn: CommandButtonComponentProps = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
@@ -154,25 +154,18 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
|
||||
}
|
||||
|
||||
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (configContext.platform === Platform.Hosted) {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
if (!container.isPreferredApiCassandra()) {
|
||||
const label = "Settings";
|
||||
const settingsPaneButton: CommandButtonComponentProps = {
|
||||
const buttons: CommandButtonComponentProps[] = [
|
||||
{
|
||||
iconSrc: SettingsIcon,
|
||||
iconAlt: label,
|
||||
iconAlt: "Settings",
|
||||
onCommandClick: () => container.openSettingPane(),
|
||||
commandButtonLabel: undefined,
|
||||
ariaLabel: label,
|
||||
tooltipText: label,
|
||||
ariaLabel: "Settings",
|
||||
tooltipText: "Settings",
|
||||
hasPopup: true,
|
||||
disabled: false,
|
||||
};
|
||||
buttons.push(settingsPaneButton);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
if (container.isHostedDataExplorerEnabled()) {
|
||||
const label = "Open Full Screen";
|
||||
@@ -223,8 +216,8 @@ export function createDivider(): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function areScriptsSupported(container: Explorer): boolean {
|
||||
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
||||
function areScriptsSupported(): boolean {
|
||||
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
}
|
||||
|
||||
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
|
||||
@@ -286,7 +279,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
container.openAddDatabasePane();
|
||||
container.addDatabasePane.open();
|
||||
// container.openAddDatabasePane();
|
||||
document.getElementById("linkAddDatabase").focus();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -296,7 +290,7 @@ function createNewDatabase(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";
|
||||
return {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
@@ -310,7 +304,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
} else if (container.isPreferredApiMongoDB()) {
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
const label = "New Query";
|
||||
return {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
@@ -332,8 +326,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
|
||||
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const shouldEnableScriptsCommands: boolean =
|
||||
!container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(container);
|
||||
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New Stored Procedure";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import {
|
||||
NotificationConsoleComponentProps,
|
||||
NotificationConsoleComponent,
|
||||
ConsoleDataType,
|
||||
NotificationConsoleComponent,
|
||||
NotificationConsoleComponentProps,
|
||||
} from "./NotificationConsoleComponent";
|
||||
|
||||
describe("NotificationConsoleComponent", () => {
|
||||
@@ -12,7 +12,7 @@ describe("NotificationConsoleComponent", () => {
|
||||
consoleData: undefined,
|
||||
isConsoleExpanded: false,
|
||||
inProgressConsoleDataIdToBeDeleted: "",
|
||||
setIsConsoleExpanded: (isExpanded: boolean): void => {},
|
||||
setIsConsoleExpanded: (): void => undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("NotificationConsoleComponent", () => {
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
|
||||
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message);
|
||||
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
|
||||
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)).toBe(true);
|
||||
};
|
||||
|
||||
it("renders progress notifications", () => {
|
||||
@@ -139,7 +139,7 @@ describe("NotificationConsoleComponent", () => {
|
||||
wrapper.setProps(props);
|
||||
|
||||
wrapper.find(".clearNotificationsButton").simulate("click");
|
||||
expect(!wrapper.exists(".notificationConsoleData"));
|
||||
expect(wrapper.exists(".notificationConsoleData")).toBe(true);
|
||||
});
|
||||
|
||||
it("collapses and hide content", () => {
|
||||
@@ -155,7 +155,7 @@ describe("NotificationConsoleComponent", () => {
|
||||
wrapper.setProps(props);
|
||||
|
||||
wrapper.find(".notificationConsoleHeader").simulate("click");
|
||||
expect(!wrapper.exists(".notificationConsoleContent"));
|
||||
expect(wrapper.exists(".notificationConsoleContent")).toBe(false);
|
||||
});
|
||||
|
||||
it("display latest data in header", () => {
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
* 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 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 ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
import InfoIcon from "../../../../images/info_color.svg";
|
||||
import ErrorRedIcon from "../../../../images/error_red.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 LoadingIcon from "../../../../images/loading.svg";
|
||||
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
|
||||
@@ -76,7 +77,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
public componentDidUpdate(
|
||||
prevProps: NotificationConsoleComponentProps,
|
||||
prevState: NotificationConsoleComponentState
|
||||
) {
|
||||
): void {
|
||||
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData);
|
||||
|
||||
if (
|
||||
@@ -97,7 +98,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
public setElememntRef = (element: HTMLElement) => {
|
||||
public setElememntRef = (element: HTMLElement): void => {
|
||||
this.consoleHeaderElement = element;
|
||||
};
|
||||
|
||||
@@ -116,7 +117,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
ref={this.setElememntRef}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
|
||||
onClick={() => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -135,6 +136,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<span className="numInfoItems">{numInfoItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
|
||||
<span className="consoleSplitter" />
|
||||
<span className="headerStatus">
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// Manages all the redux logic for the notebook nteract code
|
||||
// TODO: Merge with NotebookClient?
|
||||
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
|
||||
|
||||
// Vendor modules
|
||||
import {
|
||||
actions,
|
||||
AppState,
|
||||
ContentRecord,
|
||||
createHostRef,
|
||||
createKernelspecsRef,
|
||||
HostRecord,
|
||||
HostRef,
|
||||
IContentProvider,
|
||||
KernelspecsRef,
|
||||
makeAppRecord,
|
||||
makeCommsRecord,
|
||||
makeContentsRecord,
|
||||
@@ -19,23 +20,22 @@ import {
|
||||
makeJupyterHostRecord,
|
||||
makeStateRecord,
|
||||
makeTransformsRecord,
|
||||
ContentRecord,
|
||||
HostRecord,
|
||||
HostRef,
|
||||
KernelspecsRef,
|
||||
IContentProvider,
|
||||
} from "@nteract/core";
|
||||
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { Media } from "@nteract/outputs";
|
||||
import TransformVDOM from "@nteract/transform-vdom";
|
||||
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 * 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 { 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 };
|
||||
|
||||
@@ -168,8 +168,10 @@ export class NotebookClientV2 {
|
||||
"application/vnd.vega.v5+json": NullTransform,
|
||||
"application/vdom.v1+json": TransformVDOM,
|
||||
"application/json": Media.Json,
|
||||
"application/javascript": Media.JavaScript,
|
||||
"text/html": Media.HTML,
|
||||
"application/javascript": userContext.features.sandboxNotebookOutputs
|
||||
? SandboxJavaScript
|
||||
: Media.JavaScript,
|
||||
"text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : Media.HTML,
|
||||
"text/markdown": Media.Markdown,
|
||||
"text/latex": Media.LaTeX,
|
||||
"image/svg+xml": Media.SVG,
|
||||
|
||||
@@ -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 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 { 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 IFrameOutputs from "./outputs/IFrameOutputs";
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
contentRef: any;
|
||||
@@ -60,6 +62,16 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
<CodeCell id={id} contentRef={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: {
|
||||
monaco: (props: PassedEditorProps) =>
|
||||
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,
|
||||
|
||||
@@ -1,37 +1,33 @@
|
||||
import * as React from "react";
|
||||
import "./base.css";
|
||||
import "./default.css";
|
||||
|
||||
import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components";
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import { CellType } from "@nteract/commutable/src";
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
|
||||
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
|
||||
import Prompt from "./Prompt";
|
||||
import { promptContent } from "./PromptContent";
|
||||
|
||||
import { AzureTheme } from "./AzureTheme";
|
||||
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
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 { userContext } from "../../../UserContext";
|
||||
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 {
|
||||
contentRef: any;
|
||||
@@ -112,6 +108,16 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
</Prompt>
|
||||
),
|
||||
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>
|
||||
),
|
||||
|
||||
160
src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx
Normal file
160
src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import AddCollectionPane from "./AddCollectionPane";
|
||||
import Explorer from "../Explorer";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import AddCollectionPane from "./AddCollectionPane";
|
||||
|
||||
describe("Add Collection Pane", () => {
|
||||
describe("isValid()", () => {
|
||||
@@ -50,7 +51,14 @@ describe("Add Collection Pane", () => {
|
||||
});
|
||||
|
||||
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;
|
||||
addCollectionPane.partitionKey("/id");
|
||||
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", () => {
|
||||
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableCassandra" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
|
||||
|
||||
addCollectionPane.partitionKey("/id");
|
||||
|
||||
@@ -127,13 +127,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
this.partitionKey.extend({ rateLimit: 100 });
|
||||
this.partitionKeyPattern = ko.pureComputed(() => {
|
||||
if (this.container && this.container.isPreferredApiGraph()) {
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
return "^/[^/]*";
|
||||
}
|
||||
return ".*";
|
||||
});
|
||||
this.partitionKeyTitle = ko.pureComputed(() => {
|
||||
if (this.container && this.container.isPreferredApiGraph()) {
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
return "May not use composite partition key";
|
||||
}
|
||||
return "";
|
||||
@@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
|
||||
if (currentCollections >= maxCollections) {
|
||||
let typeOfContainer = "collection";
|
||||
if (this.container.isPreferredApiGraph() || this.container.isPreferredApiTable()) {
|
||||
if (userContext.apiType === "Gremlin" || this.container.isPreferredApiTable()) {
|
||||
typeOfContainer = "container";
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return "e.g., address.zipCode";
|
||||
}
|
||||
|
||||
if (this.container && !!this.container.isPreferredApiGraph()) {
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
return "e.g., /address";
|
||||
}
|
||||
|
||||
@@ -384,17 +384,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.uniqueKeysVisible = ko.pureComputed<boolean>(() => {
|
||||
if (
|
||||
this.container == null ||
|
||||
!!this.container.isPreferredApiMongoDB() ||
|
||||
!!this.container.isPreferredApiTable() ||
|
||||
!!this.container.isPreferredApiCassandra() ||
|
||||
!!this.container.isPreferredApiGraph()
|
||||
) {
|
||||
return false;
|
||||
if (userContext.apiType === "SQL") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
this.partitionKeyVisible = ko.computed<boolean>(() => {
|
||||
@@ -591,7 +585,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.container.isPreferredApiDocumentDB()) {
|
||||
if (userContext.apiType === "SQL") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -599,7 +593,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.container.isPreferredApiCassandra() && this.container.hasStorageAnalyticsAfecFeature()) {
|
||||
if (userContext.apiType === "Cassandra" && this.container.hasStorageAnalyticsAfecFeature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1011,7 +1005,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
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.");
|
||||
return false;
|
||||
}
|
||||
|
||||
460
src/Explorer/Panes/AddDatabasePane.ts
Normal file
460
src/Explorer/Panes/AddDatabasePane.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { AddDatabasePane } from ".";
|
||||
import Explorer from "../../Explorer";
|
||||
import { AddDatabasePane } from "../AddDatabasePane";
|
||||
const props = {
|
||||
explorer: new Explorer(),
|
||||
closePanel: (): void => undefined,
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,18 @@
|
||||
jest.mock("../../Common/dataAccess/deleteCollection");
|
||||
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";
|
||||
jest.mock("../../../Common/dataAccess/deleteCollection");
|
||||
jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import Explorer from "../Explorer";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { TreeNode } from "../../Contracts/ViewModels";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
import { DeleteCollectionConfirmationPanel } from "./DeleteCollectionConfirmationPanel";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { DeleteCollectionConfirmationPanel } from ".";
|
||||
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
|
||||
import DeleteFeedback from "../../../Common/DeleteFeedback";
|
||||
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
|
||||
import { DefaultAccountExperienceType } from "../../../DefaultAccountExperienceType";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
describe("Delete Collection Confirmation Pane", () => {
|
||||
describe("Explorer.isLastCollection()", () => {
|
||||
@@ -64,7 +63,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
@@ -118,7 +117,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
});
|
||||
@@ -132,8 +131,8 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedCollectionId } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
|
||||
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
|
||||
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
|
||||
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||
|
||||
wrapper.unmount();
|
||||
@@ -153,8 +152,8 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: feedbackText } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
|
||||
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true);
|
||||
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click");
|
||||
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
152
src/Explorer/Panes/DeleteCollectionConfirmationPanel/index.tsx
Normal file
152
src/Explorer/Panes/DeleteCollectionConfirmationPanel/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
<ExecuteSprocParamsPanel
|
||||
closePanel={[Function]}
|
||||
explorer={Object {}}
|
||||
storedProcedure={Object {}}
|
||||
>
|
||||
<GenericRightPaneComponent
|
||||
container={Object {}}
|
||||
@@ -1148,7 +1149,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
isAddRemoveVisible={false}
|
||||
onParamKeyChange={[Function]}
|
||||
onParamValueChange={[Function]}
|
||||
paramValue=""
|
||||
selectedKey="string"
|
||||
>
|
||||
<StyledLabelBase>
|
||||
@@ -2683,7 +2683,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
key=".0:$.1"
|
||||
label="Value"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
>
|
||||
<TextFieldBase
|
||||
autoFocus={true}
|
||||
@@ -2968,7 +2967,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
}
|
||||
validateOnLoad={true}
|
||||
value=""
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-102"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import { ExecuteSprocParamsPanel } from "./index";
|
||||
|
||||
describe("Excute Sproc Param Pane", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const fakeSproc = {} as StoredProcedure;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
storedProcedure: fakeSproc,
|
||||
closePanel: (): void => undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ import { IDropdownOption, IImageProps, Image, Stack, Text } from "office-ui-fabr
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import Explorer from "../../Explorer";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
|
||||
import { InputParameter } from "./InputParameter";
|
||||
|
||||
interface ExecuteSprocParamsPaneProps {
|
||||
explorer: Explorer;
|
||||
storedProcedure: StoredProcedure;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
@@ -23,11 +25,12 @@ interface UnwrappedExecuteSprocParam {
|
||||
|
||||
export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
|
||||
explorer,
|
||||
storedProcedure,
|
||||
closePanel,
|
||||
}: ExecuteSprocParamsPaneProps): JSX.Element => {
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
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 [formError, setFormError] = useState<string>("");
|
||||
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
|
||||
@@ -76,9 +79,15 @@ export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPanePr
|
||||
return;
|
||||
}
|
||||
setLoadingTrue();
|
||||
const sprocParams = wrappedSprocParams && wrappedSprocParams.map((sprocParam) => sprocParam.text);
|
||||
const currentSelectedSproc = explorer.findSelectedStoredProcedure();
|
||||
currentSelectedSproc.execute(sprocParams, partitionValue);
|
||||
const sprocParams =
|
||||
wrappedSprocParams &&
|
||||
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();
|
||||
closePanel();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
135
src/Explorer/Panes/GenericRightPaneComponent/index.tsx
Normal file
135
src/Explorer/Panes/GenericRightPaneComponent/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import AddCollectionPaneTemplate from "./AddCollectionPane.html";
|
||||
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
|
||||
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
|
||||
import GitHubReposPaneTemplate from "./GitHubReposPane.html";
|
||||
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
|
||||
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
|
||||
@@ -8,7 +7,6 @@ import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
|
||||
import StringInputPaneTemplate from "./StringInputPane.html";
|
||||
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
|
||||
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
|
||||
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
|
||||
|
||||
export class PaneComponent {
|
||||
constructor(data: any) {
|
||||
@@ -25,15 +23,6 @@ export class AddCollectionPaneComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCollectionConfirmationPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: PaneComponent,
|
||||
template: DeleteCollectionConfirmationPaneTemplate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GraphNewVertexPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
@@ -69,16 +58,6 @@ export class TableEditEntityPaneComponent {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TableQuerySelectPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: PaneComponent,
|
||||
template: TableQuerySelectPaneTemplate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class CassandraAddCollectionPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
|
||||
@@ -152,3 +152,6 @@
|
||||
.removeIcon {
|
||||
color: @InfoIconColor;
|
||||
}
|
||||
.column-select-view {
|
||||
margin: 20px 0px 0px 0px;
|
||||
}
|
||||
|
||||
@@ -91,21 +91,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -196,27 +181,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -507,21 +471,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -590,9 +539,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -632,27 +578,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -750,6 +675,41 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
<div
|
||||
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
|
||||
className="settingsSection"
|
||||
>
|
||||
@@ -932,21 +892,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -1037,27 +982,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -1348,21 +1272,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -1431,9 +1340,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -1473,27 +1379,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
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 Entities from "../../Tables/Entities";
|
||||
import * as TableConstants from "../../Tables/Constants";
|
||||
import { CassandraAPIDataClient, CassandraTableKey } from "../../Tables/TableDataClient";
|
||||
import * as Utilities from "../../Tables/Utilities";
|
||||
import EntityPropertyViewModel from "./EntityPropertyViewModel";
|
||||
import TableEntityPane from "./TableEntityPane";
|
||||
@@ -24,11 +25,9 @@ export default class AddTableEntityPane extends TableEntityPane {
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
this.submitButtonText("Add Entity");
|
||||
this.container.isPreferredApiCassandra.subscribe((isCassandra) => {
|
||||
if (isCassandra) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
this.submitButtonText("Add Row");
|
||||
}
|
||||
});
|
||||
this.scrollId = ko.observable<string>("addEntityScroll");
|
||||
}
|
||||
|
||||
@@ -57,7 +56,7 @@ export default class AddTableEntityPane extends TableEntityPane {
|
||||
headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey];
|
||||
}
|
||||
}
|
||||
if (this.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
(<CassandraAPIDataClient>this.container.tableDataClient)
|
||||
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
|
||||
.then((columns: CassandraTableKey[]) => {
|
||||
@@ -94,7 +93,7 @@ export default class AddTableEntityPane extends TableEntityPane {
|
||||
headers &&
|
||||
headers.forEach((key: string) => {
|
||||
if (!_.contains<string>(AddTableEntityPane._excludedFields, key)) {
|
||||
if (this.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
|
||||
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
|
||||
.map((key) => key.property);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import * as ko from "knockout";
|
||||
import _ from "underscore";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||
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 { userContext } from "../../../UserContext";
|
||||
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 {
|
||||
container: Explorer;
|
||||
@@ -21,11 +22,9 @@ export default class EditTableEntityPane extends TableEntityPane {
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
this.submitButtonText("Update Entity");
|
||||
this.container.isPreferredApiCassandra.subscribe((isCassandra) => {
|
||||
if (isCassandra) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
this.submitButtonText("Update Row");
|
||||
}
|
||||
});
|
||||
this.scrollId = ko.observable<string>("editEntityScroll");
|
||||
}
|
||||
|
||||
@@ -44,7 +43,7 @@ export default class EditTableEntityPane extends TableEntityPane {
|
||||
property !== TableEntityProcessor.keyProperties.etag &&
|
||||
property !== TableEntityProcessor.keyProperties.resourceId &&
|
||||
property !== TableEntityProcessor.keyProperties.self &&
|
||||
(!this.container.isPreferredApiCassandra() || property !== TableConstants.EntityKeyNames.RowKey)
|
||||
(userContext.apiType !== "Cassandra" || property !== TableConstants.EntityKeyNames.RowKey)
|
||||
) {
|
||||
numberOfProperties++;
|
||||
}
|
||||
@@ -93,9 +92,9 @@ export default class EditTableEntityPane extends TableEntityPane {
|
||||
key !== TableEntityProcessor.keyProperties.etag &&
|
||||
key !== TableEntityProcessor.keyProperties.resourceId &&
|
||||
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
|
||||
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
|
||||
.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)
|
||||
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
|
||||
.then((properties: CassandraTableKey[]) => {
|
||||
@@ -169,10 +168,7 @@ export default class EditTableEntityPane extends TableEntityPane {
|
||||
var updatedEntity: any = {};
|
||||
displayedAttributes &&
|
||||
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
|
||||
if (
|
||||
attribute.name() &&
|
||||
(!this.tableViewModel.queryTablesTab.container.isPreferredApiCassandra() || attribute.value() !== "")
|
||||
) {
|
||||
if (attribute.name() && (userContext.apiType !== "Cassandra" || attribute.value() !== "")) {
|
||||
var value = attribute.getPropertyValue();
|
||||
var type = attribute.type();
|
||||
if (type === TableConstants.TableType.Int64) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as ko from "knockout";
|
||||
import _ from "underscore";
|
||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||
import * as Entities from "../../Tables/Entities";
|
||||
import EntityPropertyViewModel from "./EntityPropertyViewModel";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as TableConstants from "../../Tables/Constants";
|
||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||
import * as Entities from "../../Tables/Entities";
|
||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||
import * as Utilities from "../../Tables/Utilities";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { ContextualPaneBase } from "../ContextualPaneBase";
|
||||
import EntityPropertyViewModel from "./EntityPropertyViewModel";
|
||||
|
||||
// Class with variables and functions that are common to both adding and editing entities
|
||||
export default abstract class TableEntityPane extends ContextualPaneBase {
|
||||
@@ -52,8 +53,7 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
|
||||
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
this.container.isPreferredApiCassandra.subscribe((isCassandra) => {
|
||||
if (isCassandra) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
this.edmTypes([
|
||||
TableConstants.CassandraType.Text,
|
||||
TableConstants.CassandraType.Ascii,
|
||||
@@ -72,11 +72,10 @@ export default abstract class TableEntityPane extends ContextualPaneBase {
|
||||
TableConstants.CassandraType.Tinyint,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
this.canAdd = ko.computed<boolean>(() => {
|
||||
// Cassandra can't add since the schema can't be changed once created
|
||||
if (this.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return false;
|
||||
}
|
||||
// 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 => {
|
||||
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
|
||||
const nonEditableType: boolean =
|
||||
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.resourceId &&
|
||||
key !== TableEntityProcessor.keyProperties.self &&
|
||||
(!viewModel.queryTablesTab.container.isPreferredApiCassandra() ||
|
||||
key !== TableConstants.EntityKeyNames.RowKey)
|
||||
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
|
||||
) {
|
||||
newHeaders.push(key);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
155
src/Explorer/Panes/Tables/TableQuerySelectPanel/index.tsx
Normal file
155
src/Explorer/Panes/Tables/TableQuerySelectPanel/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -91,21 +91,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -196,27 +181,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -507,21 +471,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -590,9 +539,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -632,27 +578,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "office-ui-fabric-react";
|
||||
import React, { ChangeEvent, FunctionComponent, useState } from "react";
|
||||
import { Upload } from "../../../Common/Upload";
|
||||
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { UploadDetails, UploadDetailsRecord } from "../../../workers/upload/definitions";
|
||||
import Explorer from "../../Explorer";
|
||||
import { getErrorMessage } from "../../Tables/Utilities";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
|
||||
@@ -13,12 +13,6 @@ export interface UploadItemsPaneProps {
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
interface IUploadFileData {
|
||||
numSucceeded: number;
|
||||
numFailed: number;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
const getTitle = (): string => {
|
||||
if (userContext.apiType === "Cassandra" || userContext.apiType === "Tables") {
|
||||
return "Upload Tables";
|
||||
@@ -54,7 +48,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
|
||||
selectedCollection
|
||||
?.uploadFiles(files)
|
||||
.then(
|
||||
(uploadDetails: UploadDetails) => {
|
||||
(uploadDetails) => {
|
||||
setUploadFileData(uploadDetails.data);
|
||||
setFiles(undefined);
|
||||
},
|
||||
@@ -84,6 +78,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
|
||||
onClose: closePanel,
|
||||
onSubmit,
|
||||
};
|
||||
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
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) {
|
||||
case "status":
|
||||
return <span>{item.numSucceeded + " items created, " + item.numFailed + " errors"}</span>;
|
||||
return `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
||||
default:
|
||||
return <span>{item.fileName}</span>;
|
||||
return item.fileName;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -92,21 +92,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||
"useIndexingForSharedThroughput": [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 {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -197,27 +182,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||
"title": [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 {
|
||||
"buildString": [Function],
|
||||
"container": [Circular],
|
||||
@@ -508,21 +472,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||
"databaseAccount": [Function],
|
||||
"databases": [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],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -593,9 +542,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
@@ -636,27 +582,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||
"queriesClient": QueriesClient {
|
||||
"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],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
|
||||
@@ -50,10 +50,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
return this.container.tabsManager.openedTabs.length === 0;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
while (this.subscriptions.length) {
|
||||
this.subscriptions.pop().dispose();
|
||||
@@ -62,7 +58,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
|
||||
public componentDidMount() {
|
||||
this.subscriptions.push(
|
||||
this.container.tabsManager.openedTabs.subscribe(() => this.setState({})),
|
||||
this.container.selectedNode.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 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="splashScreen">
|
||||
<div className="title">
|
||||
@@ -226,7 +227,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}
|
||||
|
||||
if (!this.container.isDatabaseNodeOrNoneSelected()) {
|
||||
if (this.container.isPreferredApiDocumentDB() || this.container.isPreferredApiGraph()) {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
@@ -255,7 +256,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
onClick: () => this.container.openBrowseQueriesPanel(),
|
||||
});
|
||||
|
||||
if (!this.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType !== "Cassandra") {
|
||||
items.push({
|
||||
iconSrc: NewStoredProcedureIcon,
|
||||
title: "New Stored Procedure",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Q from "q";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as Entities from "../Entities";
|
||||
import * as DataTableUtilities from "./DataTableUtilities";
|
||||
@@ -73,7 +74,7 @@ export default class TableCommands {
|
||||
}
|
||||
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
|
||||
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?";
|
||||
}
|
||||
if (window.confirm(deleteMessage)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Areas } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as Constants from "../Constants";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
@@ -412,10 +413,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
}
|
||||
|
||||
var entities = this.cache.data;
|
||||
if (
|
||||
this.queryTablesTab.container.isPreferredApiCassandra() &&
|
||||
DataTableUtilities.checkForDefaultHeader(this.headers)
|
||||
) {
|
||||
if (userContext.apiType === "Cassandra" && DataTableUtilities.checkForDefaultHeader(this.headers)) {
|
||||
(<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient)
|
||||
.getTableSchema(this.queryTablesTab.collection)
|
||||
.then((headers: CassandraTableKey[]) => {
|
||||
@@ -427,7 +425,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
} else {
|
||||
var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
|
||||
entities,
|
||||
this.queryTablesTab.container.isPreferredApiCassandra()
|
||||
userContext.apiType === "Cassandra"
|
||||
);
|
||||
var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers);
|
||||
if (newHeaders.length > 0) {
|
||||
@@ -512,7 +510,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
return Q.resolve(finalEntities);
|
||||
}
|
||||
);
|
||||
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
} else if (this.continuationToken && userContext.apiType === "Cassandra") {
|
||||
promise = Q(
|
||||
this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||
this.queryTablesTab.collection,
|
||||
@@ -523,7 +521,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
);
|
||||
} else {
|
||||
let query = this.sqlQuery();
|
||||
if (this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
query = this.cqlQuery();
|
||||
}
|
||||
promise = Q(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ko from "knockout";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as Constants from "../Constants";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||
@@ -70,7 +71,7 @@ export default class QueryBuilderViewModel {
|
||||
private scrollEventListener: boolean;
|
||||
|
||||
constructor(queryViewModel: QueryViewModel, tableEntityListViewModel: TableEntityListViewModel) {
|
||||
if (tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
this.edmTypes([
|
||||
Constants.CassandraType.Text,
|
||||
Constants.CassandraType.Ascii,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as ko from "knockout";
|
||||
import _ from "underscore";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as QueryBuilderConstants from "../Constants";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import * as Utilities from "../Utilities";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
|
||||
export default class QueryClauseViewModel {
|
||||
public checkedForGrouping: ko.Observable<boolean>;
|
||||
@@ -68,7 +69,7 @@ export default class QueryClauseViewModel {
|
||||
this.getValueType();
|
||||
|
||||
this.isOperaterEditable = ko.pureComputed<boolean>(() => {
|
||||
const isPreferredApiCassandra = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra();
|
||||
const isPreferredApiCassandra = userContext.apiType === "Cassandra";
|
||||
const cassandraKeys = isPreferredApiCassandra
|
||||
? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map(
|
||||
(key) => key.property
|
||||
@@ -84,7 +85,7 @@ export default class QueryClauseViewModel {
|
||||
this.field() !== "Timestamp" &&
|
||||
this.field() !== "PartitionKey" &&
|
||||
this.field() !== "RowKey" &&
|
||||
!this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()
|
||||
userContext.apiType !== "Cassandra"
|
||||
);
|
||||
|
||||
this.and_or.subscribe((value) => {
|
||||
@@ -170,7 +171,7 @@ export default class QueryClauseViewModel {
|
||||
this.type(QueryBuilderConstants.TableType.String);
|
||||
} else {
|
||||
this.resetFromTimestamp();
|
||||
if (this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection
|
||||
.cassandraSchema;
|
||||
for (let i = 0, len = cassandraSchema.length; i < len; i++) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as ko from "knockout";
|
||||
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 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 {
|
||||
public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
|
||||
@@ -47,7 +47,7 @@ export default class QueryViewModel {
|
||||
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel();
|
||||
|
||||
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
|
||||
return !this.queryTablesTab.container.isPreferredApiCassandra();
|
||||
return userContext.apiType !== "Cassandra";
|
||||
});
|
||||
let initialOptions = this._tableEntityListViewModel.headers;
|
||||
this.columnOptions = ko.observableArray<string>(initialOptions);
|
||||
@@ -127,7 +127,7 @@ export default class QueryViewModel {
|
||||
private setFilter = (): string => {
|
||||
var queryString = this.isEditorActive()
|
||||
? this.queryText()
|
||||
: this.queryTablesTab.container.isPreferredApiCassandra()
|
||||
: userContext.apiType === "Cassandra"
|
||||
? this.queryBuilderViewModel().getCqlFilterFromClauses()
|
||||
: this.queryBuilderViewModel().getODataFilterFromClauses();
|
||||
var filter = queryString;
|
||||
@@ -160,7 +160,7 @@ export default class QueryViewModel {
|
||||
|
||||
public runQuery = (): DataTables.DataTable => {
|
||||
var filter = this.setFilter();
|
||||
if (filter && !this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
if (filter && userContext.apiType !== "Cassandra") {
|
||||
filter = filter.replace(/"/g, "'");
|
||||
}
|
||||
var top = this.topValue();
|
||||
@@ -198,8 +198,7 @@ export default class QueryViewModel {
|
||||
};
|
||||
|
||||
public selectQueryOptions(): Promise<any> {
|
||||
this.queryTablesTab.container.querySelectPane.queryViewModel = this;
|
||||
this.queryTablesTab.container.querySelectPane.open();
|
||||
this.queryTablesTab.container.openTableSelectQueryPanel(this);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ describe("Documents tab", () => {
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
@@ -89,8 +87,6 @@ describe("Documents tab", () => {
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
@@ -106,8 +102,6 @@ describe("Documents tab", () => {
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
@@ -123,8 +117,6 @@ describe("Documents tab", () => {
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
@@ -140,8 +132,6 @@ describe("Documents tab", () => {
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
@@ -157,8 +147,6 @@ describe("Documents tab", () => {
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||
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 * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import {
|
||||
createDocument,
|
||||
deleteDocument,
|
||||
@@ -16,10 +11,14 @@ import {
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "../../Common/MongoProxyClient";
|
||||
import { extractPartitionKey } from "@azure/cosmos";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { PartitionKeyDefinition } from "@azure/cosmos";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import MongoUtility from "../../Common/MongoUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
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 {
|
||||
public collection: ViewModels.Collection;
|
||||
|
||||
@@ -88,7 +88,6 @@ export default class NotebookTabV2 extends TabsBase {
|
||||
public onCloseTabButtonClick(): Q.Promise<any> {
|
||||
const cleanup = () => {
|
||||
this.notebookComponentAdapter.notebookShutdown();
|
||||
this.isActive(false);
|
||||
super.onCloseTabButtonClick();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import QueryTab from "./QueryTab";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
describe("Query Tab", () => {
|
||||
function getNewQueryTabForContainer(container: Explorer): QueryTab {
|
||||
@@ -24,7 +25,6 @@ describe("Query Tab", () => {
|
||||
database: database,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {},
|
||||
});
|
||||
@@ -52,13 +52,19 @@ describe("Query Tab", () => {
|
||||
});
|
||||
|
||||
it("should be true for accounts using SQL API", () => {
|
||||
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
|
||||
updateUserContext({});
|
||||
const queryTab = getNewQueryTabForContainer(explorer);
|
||||
expect(queryTab.isQueryMetricsEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(queryTab.isQueryMetricsEnabled()).toBe(false);
|
||||
});
|
||||
@@ -72,13 +78,19 @@ describe("Query Tab", () => {
|
||||
});
|
||||
|
||||
it("should be visible when using a supported API", () => {
|
||||
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB);
|
||||
updateUserContext({});
|
||||
const queryTab = getNewQueryTabForContainer(explorer);
|
||||
expect(queryTab.saveQueryButton.visible()).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(queryTab.saveQueryButton.visible()).toBe(false);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as QueryUtils from "../../Utils/QueryUtils";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import template from "./QueryTab.html";
|
||||
@@ -95,9 +96,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))
|
||||
);
|
||||
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
|
||||
return (
|
||||
(this.collection && this.collection.container && this.collection.container.isPreferredApiDocumentDB()) || false
|
||||
);
|
||||
return userContext.apiType === "SQL" || false;
|
||||
});
|
||||
this.activityId = ko.observable<string>();
|
||||
this.roundTrips = ko.observable<number>();
|
||||
@@ -117,7 +116,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
|
||||
this._isSaveQueriesEnabled = ko.computed<boolean>(() => {
|
||||
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 () {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import TabsBase from "./TabsBase";
|
||||
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
|
||||
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
|
||||
import TableCommands from "../Tables/DataTable/TableCommands";
|
||||
import { TableDataClient } from "../Tables/TableDataClient";
|
||||
|
||||
import AddEntityIcon from "../../../images/AddEntity.svg";
|
||||
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
|
||||
import EditEntityIcon from "../../../images/Edit-entity.svg";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
|
||||
import QueryTextIcon from "../../../images/Query-Text.svg";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
import AddEntityIcon from "../../../images/AddEntity.svg";
|
||||
import EditEntityIcon from "../../../images/Edit-entity.svg";
|
||||
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
|
||||
import Explorer from "../Explorer";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
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 TabsBase from "./TabsBase";
|
||||
|
||||
// Will act as table explorer class
|
||||
export default class QueryTablesTab extends TabsBase {
|
||||
@@ -176,7 +176,7 @@ export default class QueryTablesTab extends TabsBase {
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
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({
|
||||
iconSrc: QueryBuilderIcon,
|
||||
iconAlt: label,
|
||||
@@ -190,7 +190,7 @@ export default class QueryTablesTab extends TabsBase {
|
||||
}
|
||||
|
||||
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({
|
||||
iconSrc: QueryTextIcon,
|
||||
iconAlt: label,
|
||||
@@ -217,7 +217,7 @@ export default class QueryTablesTab extends TabsBase {
|
||||
}
|
||||
|
||||
if (this.addEntityButton.visible()) {
|
||||
const label = this.container.isPreferredApiCassandra() ? "Add Row" : "Add Entity";
|
||||
const label = userContext.apiType === "Cassandra" ? "Add Row" : "Add Entity";
|
||||
buttons.push({
|
||||
iconSrc: AddEntityIcon,
|
||||
iconAlt: label,
|
||||
@@ -230,7 +230,7 @@ export default class QueryTablesTab extends TabsBase {
|
||||
}
|
||||
|
||||
if (this.editEntityButton.visible()) {
|
||||
const label = this.container.isPreferredApiCassandra() ? "Edit Row" : "Edit Entity";
|
||||
const label = userContext.apiType === "Cassandra" ? "Edit Row" : "Edit Entity";
|
||||
buttons.push({
|
||||
iconSrc: EditEntityIcon,
|
||||
iconAlt: label,
|
||||
@@ -243,7 +243,7 @@ export default class QueryTablesTab extends TabsBase {
|
||||
}
|
||||
|
||||
if (this.deleteEntityButton.visible()) {
|
||||
const label = this.container.isPreferredApiCassandra() ? "Delete Rows" : "Delete Entities";
|
||||
const label = userContext.apiType === "Cassandra" ? "Delete Rows" : "Delete Entities";
|
||||
buttons.push({
|
||||
iconSrc: DeleteEntitiesIcon,
|
||||
iconAlt: label,
|
||||
|
||||
@@ -208,7 +208,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
this.collection && this.collection.container.openExecuteSprocParamsPanel();
|
||||
this.collection && this.collection.container.openExecuteSprocParamsPanel(this.node);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
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 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 Explorer from "../Explorer";
|
||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||
import { TabsManager } from "./TabsManager";
|
||||
|
||||
// TODO: Use specific actions for logging telemetry data
|
||||
export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
@@ -20,18 +21,16 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
public database: ViewModels.Database;
|
||||
public rid: string;
|
||||
public hasFocus: ko.Observable<boolean>;
|
||||
public isActive: ko.Observable<boolean>;
|
||||
public isMouseOver: ko.Observable<boolean>;
|
||||
public tabId: string;
|
||||
public tabKind: ViewModels.CollectionTabKind;
|
||||
public tabTitle: ko.Observable<string>;
|
||||
public tabPath: ko.Observable<string>;
|
||||
public closeButtonTabIndex: ko.Computed<number>;
|
||||
public errorDetailsTabIndex: ko.Computed<number>;
|
||||
public hashLocation: ko.Observable<string>;
|
||||
public isExecutionError: ko.Observable<boolean>;
|
||||
public isExecuting: ko.Observable<boolean>;
|
||||
public pendingNotification?: ko.Observable<DataModels.Notification>;
|
||||
public manager?: TabsManager;
|
||||
|
||||
protected _theme: string;
|
||||
public onLoadStartKey: number;
|
||||
@@ -46,7 +45,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
this.database = options.database;
|
||||
this.rid = options.rid || (this.collection && this.collection.rid) || "";
|
||||
this.hasFocus = ko.observable<boolean>(false);
|
||||
this.isActive = options.isActive || ko.observable<boolean>(false);
|
||||
this.isMouseOver = ko.observable<boolean>(false);
|
||||
this.tabId = `tab${id}`;
|
||||
this.tabKind = options.tabKind;
|
||||
@@ -55,21 +53,12 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
(options.tabPath && ko.observable<string>(options.tabPath)) ||
|
||||
(this.collection &&
|
||||
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.isExecuting = ko.observable<boolean>(false);
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||
this.onLoadStartKey = options.onLoadStartKey;
|
||||
this.hashLocation = ko.observable<string>(options.hashLocation || "");
|
||||
this.hashLocation.subscribe((newLocation: string) => this.updateGlobalHash(newLocation));
|
||||
|
||||
this.isActive.subscribe((isActive: boolean) => {
|
||||
if (isActive) {
|
||||
this.onActivate();
|
||||
}
|
||||
});
|
||||
|
||||
this.closeTabButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
@@ -82,12 +71,9 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
const explorer = this.getContainer();
|
||||
explorer.tabsManager.closeTab(this.tabId, explorer);
|
||||
|
||||
this.manager?.closeTab(this);
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||
tabName: this.constructor.name,
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
tabId: this.tabId,
|
||||
@@ -95,7 +81,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
this.getContainer().tabsManager.activateTab(this);
|
||||
this.manager?.activateTab(this);
|
||||
}
|
||||
|
||||
protected updateSelectedNode(): void {
|
||||
@@ -127,6 +113,11 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
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 {
|
||||
this.updateSelectedNode();
|
||||
if (!!this.collection) {
|
||||
|
||||
@@ -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 -->
|
||||
<div class="nav-tabs-margin">
|
||||
<ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
@@ -8,12 +12,12 @@
|
||||
data-bind="
|
||||
attr: {
|
||||
title: $data.tabPath,
|
||||
'aria-selected': $data.isActive,
|
||||
'aria-expanded': $data.isActive,
|
||||
'aria-selected': $parent.activeTab() === $data,
|
||||
'aria-expanded': $parent.activeTab() === $data,
|
||||
'aria-controls': $data.tabId
|
||||
},
|
||||
css:{
|
||||
active: $data.isActive
|
||||
active: $parent.activeTab() === $data
|
||||
},
|
||||
hasFocus: $data.hasFocus,
|
||||
event: { keypress: onKeyPressActivate },
|
||||
@@ -33,8 +37,8 @@
|
||||
data-bind="
|
||||
click: onErrorDetailsClick,
|
||||
event: { keypress: onErrorDetailsKeyPress },
|
||||
attr: { tabindex: errorDetailsTabIndex },
|
||||
css: { actionsEnabled: isActive },
|
||||
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
|
||||
css: { actionsEnabled: $parent.activeTab() === $data },
|
||||
visible: isExecutionError"
|
||||
>
|
||||
<span class="errorIcon"></span>
|
||||
@@ -56,11 +60,14 @@
|
||||
data-bind="
|
||||
click: $data.onCloseTabButtonClick,
|
||||
event: { keypress: onKeyPressClose },
|
||||
attr: { tabindex: $data.closeButtonTabIndex },
|
||||
visible: $data.isActive() || $data.isMouseOver()"
|
||||
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
|
||||
visible: $parent.activeTab() === $data || $data.isMouseOver()"
|
||||
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" />
|
||||
</span>
|
||||
</span>
|
||||
@@ -77,7 +84,7 @@
|
||||
<!-- Tabs Panes -- Start -->
|
||||
<div class="tabPanesContainer">
|
||||
<!-- ko foreach: openedTabs -->
|
||||
<div class="tabs-container" data-bind="visible: $data.isActive">
|
||||
<div class="tabs-container" data-bind="visible: $parent.activeTab() === $data">
|
||||
<span
|
||||
data-bind="class: $data.constructor.component.name, component: { name: $data.constructor.component.name, params: $data }"
|
||||
></span>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { TabsManager } from "./TabsManager";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer";
|
||||
import QueryTab from "./QueryTab";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
import QueryTab from "./QueryTab";
|
||||
import { TabsManager } from "./TabsManager";
|
||||
|
||||
describe("Tabs manager tests", () => {
|
||||
let tabsManager: TabsManager;
|
||||
@@ -50,7 +50,6 @@ describe("Tabs manager tests", () => {
|
||||
database,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: undefined,
|
||||
});
|
||||
@@ -63,7 +62,6 @@ describe("Tabs manager tests", () => {
|
||||
title: "",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
onUpdateTabsButtons: undefined,
|
||||
});
|
||||
|
||||
@@ -72,10 +70,7 @@ describe("Tabs manager tests", () => {
|
||||
documentsTab.tabId = "2";
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tabsManager = new TabsManager();
|
||||
explorer.tabsManager = tabsManager;
|
||||
});
|
||||
beforeEach(() => (tabsManager = new TabsManager()));
|
||||
|
||||
it("open new tabs", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
@@ -122,7 +117,7 @@ describe("Tabs manager tests", () => {
|
||||
tabsManager.activateNewTab(queryTab);
|
||||
tabsManager.activateNewTab(documentsTab);
|
||||
|
||||
tabsManager.closeTab(documentsTab.tabId, explorer);
|
||||
tabsManager.closeTab(documentsTab);
|
||||
expect(tabsManager.openedTabs().length).toBe(1);
|
||||
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
|
||||
expect(tabsManager.activeTab()).toEqual(queryTab);
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export class TabsManager {
|
||||
public openedTabs: ko.ObservableArray<TabsBase>;
|
||||
public activeTab: ko.Observable<TabsBase>;
|
||||
|
||||
constructor() {
|
||||
this.openedTabs = ko.observableArray<TabsBase>([]);
|
||||
this.activeTab = ko.observable<TabsBase>();
|
||||
}
|
||||
public openedTabs = ko.observableArray<TabsBase>([]);
|
||||
public activeTab = ko.observable<TabsBase>();
|
||||
|
||||
public activateNewTab(tab: TabsBase): void {
|
||||
this.openedTabs.push(tab);
|
||||
@@ -18,66 +12,43 @@ export class TabsManager {
|
||||
}
|
||||
|
||||
public activateTab(tab: TabsBase): void {
|
||||
this.activeTab() && this.activeTab().isActive(false);
|
||||
tab.isActive(true);
|
||||
if (this.openedTabs().includes(tab)) {
|
||||
tab.manager = this;
|
||||
this.activeTab(tab);
|
||||
tab.onActivate();
|
||||
}
|
||||
}
|
||||
|
||||
public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] {
|
||||
return this.openedTabs().filter((openedTab: TabsBase) => {
|
||||
return openedTab.tabKind === tabKind && (!comparator || comparator(openedTab));
|
||||
});
|
||||
return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab)));
|
||||
}
|
||||
|
||||
public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void {
|
||||
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
|
||||
this.openedTabs().forEach((tab: TabsBase) => {
|
||||
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));
|
||||
this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate();
|
||||
}
|
||||
|
||||
public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void {
|
||||
this.activeTab() && this.activeTab().isActive(false);
|
||||
this.activeTab(undefined);
|
||||
this.openedTabs().forEach((tab: TabsBase) => {
|
||||
if (comparator(tab)) {
|
||||
tab.onCloseTabButtonClick();
|
||||
}
|
||||
});
|
||||
this.openedTabs()
|
||||
.filter(comparator)
|
||||
.forEach((tab) => tab.onCloseTabButtonClick());
|
||||
}
|
||||
|
||||
public closeTabs(): void {
|
||||
this.openedTabs([]);
|
||||
}
|
||||
|
||||
public closeTab(tabId: string, explorer: Explorer): void {
|
||||
const tabIndex: number = this.openedTabs().findIndex((tab: TabsBase) => tab.tabId === tabId);
|
||||
public closeTab(tab: TabsBase): void {
|
||||
const tabIndex = this.openedTabs().indexOf(tab);
|
||||
if (tabIndex !== -1) {
|
||||
const tabToActive: TabsBase = this.openedTabs()[tabIndex + 1] || this.openedTabs()[tabIndex - 1];
|
||||
this.openedTabs()[tabIndex].isActive(false);
|
||||
this.removeTabById(tabId);
|
||||
if (tabToActive) {
|
||||
tabToActive.isActive(true);
|
||||
this.activeTab(tabToActive);
|
||||
} else {
|
||||
explorer.selectedNode(undefined);
|
||||
explorer.onUpdateTabsButtons([]);
|
||||
this.openedTabs.remove(tab);
|
||||
tab.manager = undefined;
|
||||
|
||||
if (this.openedTabs().length === 0) {
|
||||
this.activeTab(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isTabActive(tabKind: ViewModels.CollectionTabKind): boolean {
|
||||
return this.activeTab() && this.activeTab().tabKind === tabKind;
|
||||
if (tab === this.activeTab()) {
|
||||
const tabToTheRight = this.openedTabs()[tabIndex];
|
||||
const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1];
|
||||
this.activateTab(tabToTheRight ?? lastOpenTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Collection from "./Collection";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import Explorer from "../Explorer";
|
||||
import Collection from "./Collection";
|
||||
jest.mock("monaco-editor");
|
||||
|
||||
describe("Collection", () => {
|
||||
@@ -35,18 +34,11 @@ describe("Collection", () => {
|
||||
mockContainer.isPreferredApiMongoDB = ko.computed(() => {
|
||||
return false;
|
||||
});
|
||||
mockContainer.isPreferredApiCassandra = ko.computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
mockContainer.isDatabaseNodeOrNoneSelected = () => {
|
||||
return false;
|
||||
};
|
||||
mockContainer.isPreferredApiDocumentDB = ko.computed(() => {
|
||||
return true;
|
||||
});
|
||||
mockContainer.isPreferredApiGraph = ko.computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
||||
|
||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import UploadWorker from "worker-loader!../../workers/upload";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
||||
@@ -13,16 +12,14 @@ import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefine
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
|
||||
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 { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
|
||||
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
||||
import ConflictsTab from "../Tabs/ConflictsTab";
|
||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||
@@ -196,7 +193,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
.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.showTriggers = 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>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Documents`,
|
||||
@@ -352,7 +348,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
conflictIds: ko.observableArray<ConflictId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Conflicts,
|
||||
title: "Conflicts",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Conflicts`,
|
||||
@@ -377,7 +372,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
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) => {
|
||||
this.cassandraKeys = keys;
|
||||
});
|
||||
@@ -394,7 +389,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
} else {
|
||||
this.documentIds([]);
|
||||
let title = `Entities`;
|
||||
if (this.container.isPreferredApiCassandra()) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
title = `Rows`;
|
||||
}
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
|
||||
@@ -409,12 +404,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
tabKind: ViewModels.CollectionTabKind.QueryTables,
|
||||
title: title,
|
||||
tabPath: "",
|
||||
|
||||
collection: this,
|
||||
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`,
|
||||
isActive: ko.observable(false),
|
||||
onLoadStartKey: startKey,
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
});
|
||||
@@ -466,7 +458,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
||||
collectionId: this.id(),
|
||||
isActive: ko.observable(false),
|
||||
databaseId: this.databaseId,
|
||||
isTabsContentExpanded: this.container.isTabsContentExpanded,
|
||||
onLoadStartKey: startKey,
|
||||
@@ -513,12 +504,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Documents",
|
||||
tabPath: "",
|
||||
|
||||
collection: this,
|
||||
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`,
|
||||
isActive: ko.observable(false),
|
||||
onLoadStartKey: startKey,
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
});
|
||||
@@ -561,7 +549,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collection: this,
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
};
|
||||
|
||||
@@ -604,7 +591,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collection: this,
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
|
||||
isActive: ko.observable(false),
|
||||
queryText: queryText,
|
||||
partitionKey: collection.partitionKey,
|
||||
onLoadStartKey: startKey,
|
||||
@@ -634,7 +620,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collection: this,
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`,
|
||||
isActive: ko.observable(false),
|
||||
partitionKey: collection.partitionKey,
|
||||
onLoadStartKey: startKey,
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
@@ -666,7 +651,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
||||
collectionId: this.id(),
|
||||
isActive: ko.observable(false),
|
||||
databaseId: this.databaseId,
|
||||
isTabsContentExpanded: this.container.isTabsContentExpanded,
|
||||
onLoadStartKey: startKey,
|
||||
@@ -685,7 +669,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collection: this,
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
});
|
||||
|
||||
@@ -960,73 +943,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
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> {
|
||||
if (!this.container) {
|
||||
return undefined;
|
||||
@@ -1062,13 +978,13 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
}
|
||||
|
||||
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
|
||||
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
|
||||
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
||||
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
||||
|
||||
return { data };
|
||||
}
|
||||
|
||||
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||
private uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||
const reader = new FileReader();
|
||||
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
|
||||
const fileData: string = evt.target.result;
|
||||
@@ -1079,6 +995,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
resolve({
|
||||
fileName: file.name,
|
||||
numSucceeded: 0,
|
||||
numThrottled: 0,
|
||||
numFailed: 1,
|
||||
errors: [(evt as any).error.message],
|
||||
});
|
||||
@@ -1096,21 +1013,47 @@ export default class Collection implements ViewModels.Collection {
|
||||
fileName: fileName,
|
||||
numSucceeded: 0,
|
||||
numFailed: 0,
|
||||
numThrottled: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const content = JSON.parse(documentContent);
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
await Promise.all(
|
||||
content.map(async (documentContent) => {
|
||||
await createDocument(this, documentContent);
|
||||
record.numSucceeded++;
|
||||
})
|
||||
const parsedContent = JSON.parse(documentContent);
|
||||
if (Array.isArray(parsedContent)) {
|
||||
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
|
||||
const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) =>
|
||||
parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize)
|
||||
);
|
||||
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 {
|
||||
await createDocument(this, documentContent);
|
||||
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 {
|
||||
await createDocument(this, parsedContent);
|
||||
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
|
||||
*/
|
||||
@@ -1163,10 +1072,10 @@ export default class Collection implements ViewModels.Collection {
|
||||
if (this.container.isPreferredApiTable()) {
|
||||
this.onTableEntitiesClick();
|
||||
return;
|
||||
} else if (this.container.isPreferredApiCassandra()) {
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
this.onTableEntitiesClick();
|
||||
return;
|
||||
} else if (this.container.isPreferredApiGraph()) {
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
this.onGraphDocumentsClick();
|
||||
return;
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
@@ -1183,9 +1092,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
public getLabel(): string {
|
||||
if (this.container.isPreferredApiTable()) {
|
||||
return "Entities";
|
||||
} else if (this.container.isPreferredApiCassandra()) {
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
return "Rows";
|
||||
} else if (this.container.isPreferredApiGraph()) {
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
return "Graph";
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||
@@ -78,7 +79,6 @@ export default class Database implements ViewModels.Database {
|
||||
rid: this.rid,
|
||||
database: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`,
|
||||
isActive: ko.observable(false),
|
||||
onLoadStartKey: startKey,
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
};
|
||||
@@ -172,6 +172,27 @@ export default class Database implements ViewModels.Database {
|
||||
public async loadCollections(): Promise<void> {
|
||||
const collectionVMs: Collection[] = [];
|
||||
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);
|
||||
|
||||
collections.forEach((collection: DataModels.Collection) => {
|
||||
|
||||
@@ -92,7 +92,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
collection: this,
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
|
||||
isActive: ko.observable(false),
|
||||
queryText: queryText,
|
||||
partitionKey: collection.partitionKey,
|
||||
resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(),
|
||||
@@ -139,7 +138,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Documents`,
|
||||
|
||||
@@ -253,7 +253,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
* @param container
|
||||
*/
|
||||
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 {
|
||||
@@ -273,7 +273,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
|
||||
});
|
||||
|
||||
if (!this.container.isPreferredApiCassandra() || !this.container.isServerlessEnabled()) {
|
||||
if (userContext.apiType !== "Cassandra" || !this.container.isServerlessEnabled()) {
|
||||
children.push({
|
||||
label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings",
|
||||
onClick: collection.onSettingsClick.bind(collection),
|
||||
|
||||
@@ -76,7 +76,6 @@ export default class StoredProcedure {
|
||||
collection: source,
|
||||
node: source,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/sproc`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
|
||||
});
|
||||
|
||||
@@ -123,7 +122,6 @@ export default class StoredProcedure {
|
||||
this.collection.databaseId,
|
||||
this.collection.id()
|
||||
)}/sprocs/${this.id()}`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
});
|
||||
|
||||
@@ -138,7 +136,7 @@ export default class StoredProcedure {
|
||||
|
||||
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);
|
||||
},
|
||||
(reason) => {}
|
||||
|
||||
@@ -58,7 +58,6 @@ export default class Trigger {
|
||||
collection: source,
|
||||
node: source,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
|
||||
});
|
||||
|
||||
@@ -98,7 +97,6 @@ export default class Trigger {
|
||||
this.collection.databaseId,
|
||||
this.collection.id()
|
||||
)}/triggers/${this.id()}`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
});
|
||||
|
||||
@@ -113,7 +111,7 @@ export default class Trigger {
|
||||
|
||||
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);
|
||||
},
|
||||
(reason) => {}
|
||||
|
||||
@@ -44,7 +44,6 @@ export default class UserDefinedFunction {
|
||||
collection: source,
|
||||
node: source,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
|
||||
});
|
||||
|
||||
@@ -82,7 +81,6 @@ export default class UserDefinedFunction {
|
||||
this.collection.databaseId,
|
||||
this.collection.id()
|
||||
)}/udfs/${this.id()}`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||
});
|
||||
|
||||
@@ -106,7 +104,7 @@ export default class UserDefinedFunction {
|
||||
|
||||
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);
|
||||
},
|
||||
(reason) => {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user