Compare commits

..

7 Commits

Author SHA1 Message Date
Laurent Nguyen
a17d71d76e Merge branch 'master' into languy-resource-tree-to-react 2021-03-16 16:27:30 +01:00
Laurent Nguyen
faf2e3b559 Remove comment and test code 2021-03-15 17:35:35 +01:00
Laurent Nguyen
7e992c2b17 Fix build issues and reformat 2021-03-12 14:01:35 +01:00
Laurent Nguyen
21b92ed4f8 Merge branch 'master' into languy-resource-tree-to-react 2021-03-12 11:21:01 +01:00
Laurent Nguyen
e5755dff39 Fix unit tests 2021-03-12 10:36:27 +01:00
Laurent Nguyen
e48a6a10cb Fix notebook updates issues 2021-03-11 16:22:59 +01:00
Laurent Nguyen
4480a7250d Remove ResourceTreeAdapter 2021-03-08 14:20:27 +01:00
192 changed files with 8197 additions and 23243 deletions

View File

@@ -24,6 +24,7 @@ src/Common/ObjectCache.test.ts
src/Common/ObjectCache.ts src/Common/ObjectCache.ts
src/Common/QueriesClient.ts src/Common/QueriesClient.ts
src/Common/Splitter.ts src/Common/Splitter.ts
src/Common/UrlUtility.ts
src/Config.ts src/Config.ts
src/Contracts/ActionContracts.ts src/Contracts/ActionContracts.ts
src/Contracts/DataModels.ts src/Contracts/DataModels.ts
@@ -97,6 +98,7 @@ src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
src/Explorer/Menus/ContextMenu.ts src/Explorer/Menus/ContextMenu.ts
src/Explorer/MostRecentActivity/MostRecentActivity.ts src/Explorer/MostRecentActivity/MostRecentActivity.ts
src/Explorer/Notebook/FileSystemUtil.ts
src/Explorer/Notebook/NotebookClientV2.ts src/Explorer/Notebook/NotebookClientV2.ts
src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts
src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts
@@ -125,12 +127,15 @@ src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.ts src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/ExecuteSprocParamsPane.ts
src/Explorer/Panes/GraphStylingPane.ts src/Explorer/Panes/GraphStylingPane.ts
src/Explorer/Panes/LoadQueryPane.ts src/Explorer/Panes/LoadQueryPane.ts
src/Explorer/Panes/NewVertexPane.ts src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SaveQueryPane.ts src/Explorer/Panes/SaveQueryPane.ts
src/Explorer/Panes/SettingsPane.test.ts
src/Explorer/Panes/SettingsPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/StringInputPane.ts src/Explorer/Panes/StringInputPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/SwitchDirectoryPane.ts
@@ -143,6 +148,8 @@ src/Explorer/Panes/Tables/TableEntityPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
src/Explorer/Panes/UploadFilePane.ts
src/Explorer/Panes/UploadItemsPane.ts
src/Explorer/SplashScreen/SplashScreen.test.ts src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts src/Explorer/Tables/DataTable/CacheBase.ts
@@ -249,8 +256,11 @@ src/Terminal/NotebookAppContracts.d.ts
src/Terminal/index.ts src/Terminal/index.ts
src/TokenProviders/PortalTokenProvider.ts src/TokenProviders/PortalTokenProvider.ts
src/TokenProviders/TokenProviderFactory.ts src/TokenProviders/TokenProviderFactory.ts
src/Utils/DatabaseAccountUtils.test.ts
src/Utils/DatabaseAccountUtils.ts
src/Utils/PricingUtils.test.ts src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts
src/applyExplorerBindings.ts src/applyExplorerBindings.ts
src/global.d.ts src/global.d.ts
src/setupTests.ts src/setupTests.ts

View File

@@ -1,9 +0,0 @@
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"

View File

@@ -18,6 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
### Hosted Development (https://cosmos.azure.com) ### Hosted Development (https://cosmos.azure.com)
- Visit: `https://localhost:1234/hostedExplorer.html` - Visit: `https://localhost:1234/hostedExplorer.html`
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine. - The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
### Emulator Development ### Emulator Development
@@ -68,7 +69,7 @@ Jest and Puppeteer are used for end to end browser based tests and are contained
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details. We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
### Architecture ### Architechture
[![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0) [![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)

View File

@@ -7,6 +7,5 @@ module.exports = {
defaultViewport: null, defaultViewport: null,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
args: ["--disable-web-security"], args: ["--disable-web-security"],
exitOnPageError: false,
}, },
}; };

View File

@@ -21,13 +21,17 @@ module.exports = {
collectCoverage: true, collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected // An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"], // collectCoverageFrom: [
// "src/Common/Headers*"
// ],
// The directory where Jest should output its coverage files // The directory where Jest should output its coverage files
coverageDirectory: "coverage", coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection // An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ["/node_modules/"], // coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// A list of reporter names that Jest uses when writing coverage reports // A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ["json", "text", "cobertura"], coverageReporters: ["json", "text", "cobertura"],
@@ -35,10 +39,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results // An object that configures minimum threshold enforcement for coverage results
coverageThreshold: { coverageThreshold: {
global: { global: {
branches: 25, branches: 22,
functions: 25, functions: 28,
lines: 30, lines: 33,
statements: 30, statements: 31,
}, },
}, },
@@ -67,8 +71,7 @@ module.exports = {
// A map from regular expressions to module names that allow to stub out resources with a single module // A map from regular expressions to module names that allow to stub out resources with a single module
moduleNameMapper: { moduleNameMapper: {
"^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule", "^.*[.](svg|png|gif|less)$": "<rootDir>/mockModule",
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
"worker-loader": "<rootDir>/mockModule", "worker-loader": "<rootDir>/mockModule",
"office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes
"^dnd-core$": "dnd-core/dist/cjs", "^dnd-core$": "dnd-core/dist/cjs",

View File

@@ -718,7 +718,7 @@ execute-sproc-params-pane {
} }
} }
.stored-procedure-tab { stored-procedure-tab {
@ToggleHeight: 30px; @ToggleHeight: 30px;
@ToggleWidth: 180px; @ToggleWidth: 180px;

View File

@@ -7,6 +7,7 @@
.main { .main {
height: 100%; height: 100%;
} }
border-right: 1px solid @BaseMedium;
} }
.resourceTreeScroll { .resourceTreeScroll {

3084
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -43,9 +43,10 @@
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110", "@uifabric/react-cards": "0.109.110",
"@uifabric/react-hooks": "7.14.0",
"@uifabric/styling": "7.13.7", "@uifabric/styling": "7.13.7",
"abort-controller": "3.0.0",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
@@ -59,6 +60,8 @@
"date-fns": "1.29.0", "date-fns": "1.29.0",
"dayjs": "1.8.19", "dayjs": "1.8.19",
"dotenv": "8.2.0", "dotenv": "8.2.0",
"es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2", "eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
"hasher": "1.2.0", "hasher": "1.2.0",
@@ -76,9 +79,12 @@
"monaco-editor": "0.18.1", "monaco-editor": "0.18.1",
"ms": "2.1.3", "ms": "2.1.3",
"msal": "1.4.4", "msal": "1.4.4",
"office-ui-fabric-react": "7.164.2", "object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1",
"p-retry": "4.2.0", "p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0",
"q": "1.5.1", "q": "1.5.1",
"react": "16.13.1", "react": "16.13.1",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
@@ -95,9 +101,13 @@
"rxjs": "6.6.3", "rxjs": "6.6.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "3.1.0", "text-encoding": "0.7.0",
"underscore": "1.9.1", "underscore": "1.9.1",
"utility-types": "3.10.0" "url-polyfill": "1.1.7",
"utility-types": "3.10.0",
"webcrypto-liner": "1.1.4",
"webfontloader": "1.6.28",
"whatwg-fetch": "3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.9.0", "@babel/core": "7.9.0",
@@ -111,15 +121,15 @@
"@types/d3": "5.9.2", "@types/d3": "5.9.2",
"@types/enzyme": "3.10.7", "@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.6",
"@types/expect-puppeteer": "4.4.5", "@types/expect-puppeteer": "4.4.3",
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/jest-environment-puppeteer": "4.4.1", "@types/jest-environment-puppeteer": "4.3.2",
"@types/memoize-one": "4.1.1", "@types/memoize-one": "4.1.1",
"@types/node": "12.11.1", "@types/node": "12.11.1",
"@types/promise.prototype.finally": "2.0.3", "@types/promise.prototype.finally": "2.0.3",
"@types/prop-types": "15.5.8", "@types/prop-types": "15.5.8",
"@types/puppeteer": "5.4.3", "@types/puppeteer": "3.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.0", "@types/react": "17.0.0",
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.0",
@@ -127,7 +137,9 @@
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.1",
"@types/text-encoding": "0.0.33",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "4.0.1", "@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1", "@typescript-eslint/parser": "4.0.1",
"axe-puppeteer": "1.1.0", "axe-puppeteer": "1.1.0",
@@ -152,6 +164,7 @@
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"inline-css": "2.2.5",
"jest": "25.5.4", "jest": "25.5.4",
"jest-canvas-mock": "2.1.0", "jest-canvas-mock": "2.1.0",
"jest-puppeteer": "4.4.0", "jest-puppeteer": "4.4.0",
@@ -163,11 +176,12 @@
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"prettier": "2.2.1", "prettier": "2.2.1",
"puppeteer": "8.0.0", "puppeteer": "4.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"terser-webpack-plugin": "3.0.5",
"ts-loader": "6.2.2", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",

View File

@@ -98,6 +98,30 @@ export class CapabilityNames {
public static readonly EnableServerless: string = "EnableServerless"; public static readonly EnableServerless: string = "EnableServerless";
} }
export class Features {
public static readonly cosmosdb = "cosmosdb";
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
public static readonly executeSproc = "dataexplorerexecutesproc";
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl";
public static readonly notebookServerToken = "notebookservertoken";
public static readonly notebookBasePath = "notebookbasepath";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey";
public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1";
public static readonly selfServeType = "selfservetype";
public static readonly enableKOPanel = "enablekopanel";
}
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase
export class Flights { export class Flights {
public static readonly SettingsV2 = "settingsv2"; public static readonly SettingsV2 = "settingsv2";
@@ -368,6 +392,9 @@ export class Notebook {
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 120000;
public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly GitHubReposTitle = "GitHub repos";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -1,8 +1,8 @@
import { MessageTypes } from "../Contracts/ExplorerContracts";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { getDataExplorerWindow } from "../Utils/WindowUtils";
import * as Constants from "./Constants"; import * as Constants from "./Constants";
import { getDataExplorerWindow } from "../Utils/WindowUtils";
export interface CachedDataPromise<T> { export interface CachedDataPromise<T> {
deferred: Q.Deferred<T>; deferred: Q.Deferred<T>;
@@ -61,21 +61,6 @@ export function sendMessage(data: any): void {
} }
} }
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(
{
signature: "pcIframe",
kind: "ready",
data: "ready",
},
portalChildWindow.document.referrer
);
}
}
export function canSendMessage(): boolean { export function canSendMessage(): boolean {
return window.parent !== window; return window.parent !== window;
} }

View File

@@ -6,7 +6,7 @@ import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../Utils/QueryUtils"; import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";

View File

@@ -1,24 +0,0 @@
import { useId } from "@uifabric/react-hooks";
import { ITooltipHostStyles, TooltipHost } from "office-ui-fabric-react/lib/Tooltip";
import * as React from "react";
import InfoBubble from "../../../images/info-bubble.svg";
const calloutProps = { gapSpace: 0 };
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: "inline-block" } };
export interface TooltipProps {
children: string;
}
export const Tooltip: React.FunctionComponent = ({ children }: TooltipProps) => {
const tooltipId = useId("tooltip");
return children ? (
<span>
<TooltipHost content={children} id={tooltipId} calloutProps={calloutProps} styles={hostStyles}>
<img className="infoImg" src={InfoBubble} alt="More information" />
</TooltipHost>
</span>
) : (
<></>
);
};

View File

@@ -1,75 +0,0 @@
import { Image, Stack, TextField } from "office-ui-fabric-react";
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
import FolderIcon from "../../../images/folder_16x16.svg";
import * as Constants from "../../Common/Constants";
import { Tooltip } from "../Tooltip";
interface UploadProps {
label: string;
accept?: string;
tooltip?: string;
multiple?: boolean;
tabIndex?: number;
onUpload: (event: ChangeEvent<HTMLInputElement>) => void;
}
export const Upload: FunctionComponent<UploadProps> = ({
label,
accept,
tooltip,
multiple,
tabIndex,
...props
}: UploadProps) => {
const [selectedFilesTitle, setSelectedFilesTitle] = useState<string[]>([]);
const fileRef = useRef<HTMLInputElement>();
const onImportLinkKeyPress = (event: KeyboardEvent<HTMLAnchorElement>): void => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
onImportLinkClick();
}
};
const onImportLinkClick = (): void => {
fileRef?.current?.click();
};
const onUpload = (event: ChangeEvent<HTMLInputElement>): void => {
const { files } = event.target;
const newFileList = [];
for (let i = 0; i < files.length; i++) {
newFileList.push(files.item(i).name);
}
if (newFileList) {
setSelectedFilesTitle(newFileList);
props.onUpload(event);
}
};
const title = label + " to upload";
return (
<div>
<span className="renewUploadItemsHeader">{label}</span>
<Tooltip>{tooltip}</Tooltip>
<Stack horizontal>
<TextField styles={{ fieldGroup: { width: 300 } }} readOnly value={selectedFilesTitle.toString()} />
<input
type="file"
id="importFileInput"
style={{ display: "none" }}
ref={fileRef}
accept={accept}
tabIndex={tabIndex}
multiple={multiple}
title="Upload Icon"
onChange={onUpload}
role="button"
/>
<a href="#" id="fileImportLinkNotebook" onClick={onImportLinkClick} onKeyPress={onImportLinkKeyPress}>
<Image className="fileImportImg" src={FolderIcon} alt={title} title={title} />
</a>
</Stack>
</div>
);
};

View File

@@ -1,61 +1,55 @@
interface Result { export default class UrlUtility {
type?: string; public static parseDocumentsPath(resourcePath: string): any {
objectBody?: { if (typeof resourcePath !== "string") {
id: string; return {};
self: string; }
};
} if (resourcePath.length === 0) {
return {};
export function parseDocumentsPath(resourcePath: string): Result { }
if (typeof resourcePath !== "string") {
return {}; if (resourcePath[resourcePath.length - 1] !== "/") {
} resourcePath = resourcePath + "/";
}
if (resourcePath.length === 0) {
return {}; if (resourcePath[0] !== "/") {
} resourcePath = "/" + resourcePath;
}
if (resourcePath[resourcePath.length - 1] !== "/") {
resourcePath = resourcePath + "/"; var id: string;
} var type: string;
var pathParts = resourcePath.split("/");
if (resourcePath[0] !== "/") {
resourcePath = "/" + resourcePath; if (pathParts.length % 2 === 0) {
} id = pathParts[pathParts.length - 2];
type = pathParts[pathParts.length - 3];
let id: string; } else {
let type: string; id = pathParts[pathParts.length - 3];
const pathParts = resourcePath.split("/"); type = pathParts[pathParts.length - 2];
}
if (pathParts.length % 2 === 0) {
id = pathParts[pathParts.length - 2]; var result = {
type = pathParts[pathParts.length - 3]; type: type,
} else { objectBody: {
id = pathParts[pathParts.length - 3]; id: id,
type = pathParts[pathParts.length - 2]; self: resourcePath,
} },
};
const result = {
type: type, return result;
objectBody: { }
id: id,
self: resourcePath, public static createUri(baseUri: string, relativeUri: string): string {
}, if (!baseUri) {
}; throw new Error("baseUri is null or empty");
}
return result;
} var slashAtEndOfUriRegex = /\/$/,
slashAtStartOfUriRegEx = /^\//;
export function createUri(baseUri: string, relativeUri: string): string {
if (!baseUri) { var normalizedBaseUri = baseUri.replace(slashAtEndOfUriRegex, "") + "/",
throw new Error("baseUri is null or empty"); normalizedRelativeUri = (relativeUri && relativeUri.replace(slashAtStartOfUriRegEx, "")) || "";
}
return normalizedBaseUri + normalizedRelativeUri;
const slashAtEndOfUriRegex = /\/$/, }
slashAtStartOfUriRegEx = /^\//;
const normalizedBaseUri = baseUri.replace(slashAtEndOfUriRegex, "") + "/",
normalizedRelativeUri = (relativeUri && relativeUri.replace(slashAtStartOfUriRegEx, "")) || "";
return normalizedBaseUri + normalizedRelativeUri;
} }

View File

@@ -88,6 +88,7 @@ export interface Database extends TreeNode {
loadCollections(): Promise<void>; loadCollections(): Promise<void>;
findCollectionWithId(collectionId: string): Collection; findCollectionWithId(collectionId: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void; openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
onSettingsClick: () => void; onSettingsClick: () => void;
loadOffer(): Promise<void>; loadOffer(): Promise<void>;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>; getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
@@ -375,6 +376,7 @@ export interface DataExplorerInputsFrame {
masterKey?: string; masterKey?: string;
hasWriteAccess?: boolean; hasWriteAccess?: boolean;
authorizationToken?: string; authorizationToken?: string;
features: { [key: string]: string };
csmEndpoint?: string; csmEndpoint?: string;
dnsSuffix?: string; dnsSuffix?: string;
serverId?: string; serverId?: string;
@@ -388,6 +390,7 @@ export interface DataExplorerInputsFrame {
sharedThroughputMaximum?: number; sharedThroughputMaximum?: number;
sharedThroughputDefault?: number; sharedThroughputDefault?: number;
dataExplorerVersion?: string; dataExplorerVersion?: string;
isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
} }

View File

@@ -1,10 +1,5 @@
import dayjs from "dayjs";
import * as Plotly from "plotly.js-cartesian-dist-min"; import * as Plotly from "plotly.js-cartesian-dist-min";
import { StyleConstants } from "../../Common/Constants"; import dayjs from "dayjs";
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import "./Heatmap.less";
import { import {
ChartSettings, ChartSettings,
DataPayload, DataPayload,
@@ -16,6 +11,11 @@ import {
PartitionTimeStampToData, PartitionTimeStampToData,
PortalTheme, PortalTheme,
} from "./HeatmapDatatypes"; } from "./HeatmapDatatypes";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { StyleConstants } from "../../Common/Constants";
import "./Heatmap.less";
export class Heatmap { export class Heatmap {
public static readonly elementId: string = "heatmap"; public static readonly elementId: string = "heatmap";
@@ -266,4 +266,4 @@ export function handleMessage(event: MessageEvent) {
} }
window.addEventListener("message", handleMessage, false); window.addEventListener("message", handleMessage, false);
sendReadyMessage(); sendMessage("ready");

View File

@@ -77,6 +77,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true); expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
}); });
it("should register delete-database-confirmation-pane component", () => {
expect(ko.components.isRegistered("delete-database-confirmation-pane")).toBe(true);
});
it("should register save-query-pane component", () => { it("should register save-query-pane component", () => {
expect(ko.components.isRegistered("save-query-pane")).toBe(true); expect(ko.components.isRegistered("save-query-pane")).toBe(true);
}); });
@@ -93,6 +97,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
}); });
it("should register upload-file-pane component", () => {
expect(ko.components.isRegistered("upload-file-pane")).toBe(true);
});
it("should register string-input-pane component", () => { it("should register string-input-pane component", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true); expect(ko.components.isRegistered("string-input-pane")).toBe(true);
}); });

View File

@@ -1,30 +1,16 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as PaneComponents from "./Panes/PaneComponents";
import * as TabComponents from "./Tabs/TabComponents";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent"; import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead"; import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent"; import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import * as PaneComponents from "./Panes/PaneComponents"; import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import ConflictsTab from "./Tabs/ConflictsTab"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import DatabaseSettingsTab from "./Tabs/DatabaseSettingsTab";
import DocumentsTab from "./Tabs/DocumentsTab";
import GalleryTab from "./Tabs/GalleryTab";
import GraphTab from "./Tabs/GraphTab";
import MongoShellTab from "./Tabs/MongoShellTab";
import NotebookTabV2 from "./Tabs/NotebookV2Tab";
import NotebookViewerTab from "./Tabs/NotebookViewerTab";
import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TabsManagerTemplate from "./Tabs/TabsManager.html";
import TerminalTab from "./Tabs/TerminalTab";
import TriggerTab from "./Tabs/TriggerTab";
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent); ko.components.register("new-vertex-form", NewVertexComponent);
@@ -35,27 +21,28 @@ ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent); ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", { template: TabsManagerTemplate }); ko.components.register("tabs-manager", TabsManagerKOComponent());
// Collection Tabs // Collection Tabs
[ ko.components.register("documents-tab", new TabComponents.DocumentsTab());
DocumentsTab, ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
StoredProcedureTab, ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
TriggerTab, ko.components.register("trigger-tab", new TabComponents.TriggerTab());
UserDefinedFunctionTab, ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
SettingsTabV2, ko.components.register("collection-settings-tab-v2", new TabComponents.SettingsTabV2());
QueryTab, ko.components.register("query-tab", new TabComponents.QueryTab());
QueryTablesTab, ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
GraphTab, ko.components.register("graph-tab", new TabComponents.GraphTab());
MongoShellTab, ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
ConflictsTab, ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
NotebookTabV2, ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab());
TerminalTab, ko.components.register("terminal-tab", new TabComponents.TerminalTab());
GalleryTab, ko.components.register("gallery-tab", new TabComponents.GalleryTab());
NotebookViewerTab, ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab());
DatabaseSettingsTab,
DatabaseSettingsTabV2, // Database Tabs
].forEach(({ component: { name, template } }) => ko.components.register(name, { template })); ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
ko.components.register("database-settings-tab-v2", new TabComponents.SettingsTabV2());
// Panes // Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
@@ -64,7 +51,10 @@ ko.components.register(
"delete-collection-confirmation-pane", "delete-collection-confirmation-pane",
new PaneComponents.DeleteCollectionConfirmationPaneComponent() new PaneComponents.DeleteCollectionConfirmationPaneComponent()
); );
ko.components.register(
"delete-database-confirmation-pane",
new PaneComponents.DeleteDatabaseConfirmationPaneComponent()
);
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent()); ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
@@ -72,9 +62,13 @@ ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEnt
ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent()); ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent());
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent()); ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent());
ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent());
ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent());
ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent()); ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent());
ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent()); ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent());
ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent()); ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent());
ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

@@ -1,22 +1,23 @@
import * as ko from "knockout";
import * as ViewModels from "../Contracts/ViewModels";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import AddCollectionIcon from "../../images/AddCollection.svg"; import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg"; import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import AddUdfIcon from "../../images/AddUdf.svg";
import DeleteCollectionIcon from "../../images/DeleteCollection.svg"; import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg"; import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg"; import AddUdfIcon from "../../images/AddUdf.svg";
import AddTriggerIcon from "../../images/AddTrigger.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg"; import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
import { userContext } from "../UserContext";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger"; import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction"; import { userContext } from "../UserContext";
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
export interface CollectionContextMenuButtonParams { export interface CollectionContextMenuButtonParams {
databaseId: string; databaseId: string;
@@ -42,7 +43,7 @@ export class ResourceTreeContextMenuButtonFactory {
if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) { if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) {
items.push({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: () => container.openDeleteDatabaseConfirmationPane(), onClick: () => container.deleteDatabaseConfirmationPane.open(),
label: container.deleteDatabaseText(), label: container.deleteDatabaseText(),
styleClass: "deleteDatabaseMenuItem", styleClass: "deleteDatabaseMenuItem",
}); });

View File

@@ -6,7 +6,6 @@ describe("CollapsibleSectionComponent", () => {
it("renders", () => { it("renders", () => {
const props: CollapsibleSectionProps = { const props: CollapsibleSectionProps = {
title: "Sample title", title: "Sample title",
isExpandedByDefault: true,
}; };
const wrapper = shallow(<CollapsibleSectionComponent {...props} />); const wrapper = shallow(<CollapsibleSectionComponent {...props} />);

View File

@@ -1,10 +1,9 @@
import { Icon, Label, Stack } from "office-ui-fabric-react"; import { Icon, Label, Stack } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; import { accordionIconStyles, accordionStackTokens } from "../Settings/SettingsRenderUtils";
export interface CollapsibleSectionProps { export interface CollapsibleSectionProps {
title: string; title: string;
isExpandedByDefault: boolean;
} }
export interface CollapsibleSectionState { export interface CollapsibleSectionState {
@@ -15,7 +14,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
constructor(props: CollapsibleSectionProps) { constructor(props: CollapsibleSectionProps) {
super(props); super(props);
this.state = { this.state = {
isExpanded: this.props.isExpandedByDefault, isExpanded: true,
}; };
} }
@@ -26,14 +25,8 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<> <>
<Stack <Stack className="collapsibleSection" horizontal tokens={accordionStackTokens} onClick={this.toggleCollapsed}>
className="collapsibleSection" <Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} styles={accordionIconStyles} />
horizontal
verticalAlign="center"
tokens={accordionStackTokens}
onClick={this.toggleCollapsed}
>
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
<Label>{this.props.title}</Label> <Label>{this.props.title}</Label>
</Stack> </Stack>
{this.state.isExpanded && this.props.children} {this.state.isExpanded && this.props.children}

View File

@@ -11,10 +11,16 @@ exports[`CollapsibleSectionComponent renders 1`] = `
"childrenGap": 10, "childrenGap": 10,
} }
} }
verticalAlign="center"
> >
<Icon <StyledIconBase
iconName="ChevronDown" iconName="ChevronDown"
styles={
Object {
"root": Object {
"paddingTop": 7,
},
}
}
/> />
<StyledLabelBase> <StyledLabelBase>
Sample title Sample title

View File

@@ -350,11 +350,12 @@ exports[`test render renders with filters 1`] = `
} }
> >
<div <div
className="ms-ScrollablePane root-72" className="ms-ScrollablePane root-40"
data-is-scrollable="true" data-is-scrollable="true"
> >
<div <div
className="stickyAbove-74" aria-hidden="true"
className="stickyAbove-42"
style={ style={
Object { Object {
"height": 0, "height": 0,
@@ -365,7 +366,7 @@ exports[`test render renders with filters 1`] = `
} }
/> />
<div <div
className="ms-ScrollablePane--contentContainer contentContainer-73" className="ms-ScrollablePane--contentContainer contentContainer-41"
data-is-scrollable={true} data-is-scrollable={true}
> >
<Sticky <Sticky
@@ -374,6 +375,7 @@ exports[`test render renders with filters 1`] = `
> >
<div> <div>
<div <div
aria-hidden={true}
style={ style={
Object { Object {
"pointerEvents": "none", "pointerEvents": "none",
@@ -393,6 +395,7 @@ exports[`test render renders with filters 1`] = `
style={Object {}} style={Object {}}
> >
<div <div
aria-hidden={false}
style={ style={
Object { Object {
"backgroundColor": "", "backgroundColor": "",
@@ -408,7 +411,6 @@ exports[`test render renders with filters 1`] = `
> >
<TextFieldBase <TextFieldBase
ariaLabel="Directory filter text box" ariaLabel="Directory filter text box"
canRevealPassword={false}
className="directoryListFilterTextBox" className="directoryListFilterTextBox"
deferredValidationTime={200} deferredValidationTime={200}
onChange={[Function]} onChange={[Function]}
@@ -691,18 +693,18 @@ exports[`test render renders with filters 1`] = `
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField directoryListFilterTextBox root-78" className="ms-TextField directoryListFilterTextBox root-46"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-79" className="ms-TextField-fieldGroup fieldGroup-47"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Directory filter text box" aria-label="Directory filter text box"
className="ms-TextField-field field-80" className="ms-TextField-field field-48"
id="TextField0" id="TextField0"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -1121,7 +1123,7 @@ exports[`test render renders with filters 1`] = `
"iconDisabled": Object { "iconDisabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"color": "GrayText", "color": "GrayText",
}, },
}, },
@@ -1147,7 +1149,7 @@ exports[`test render renders with filters 1`] = `
"menuIconDisabled": Object { "menuIconDisabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"color": "GrayText", "color": "GrayText",
}, },
}, },
@@ -1166,7 +1168,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 2, "right": 2,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
"outlineColor": "ButtonText", "outlineColor": "ButtonText",
@@ -1245,7 +1247,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 2, "right": 2,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
"outlineColor": "ButtonText", "outlineColor": "ButtonText",
@@ -1277,10 +1279,8 @@ exports[`test render renders with filters 1`] = `
}, },
}, },
Object { Object {
"backgroundColor": "#f3f2f1",
"color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1300,7 +1300,7 @@ exports[`test render renders with filters 1`] = `
"backgroundColor": "#f3f2f1", "backgroundColor": "#f3f2f1",
"color": "#201f1e", "color": "#201f1e",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"borderColor": "Highlight", "borderColor": "Highlight",
"color": "Highlight", "color": "Highlight",
}, },
@@ -1326,7 +1326,7 @@ exports[`test render renders with filters 1`] = `
"splitButtonContainer": Array [ "splitButtonContainer": Array [
Object { Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"border": "none", "border": "none",
}, },
}, },
@@ -1344,7 +1344,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 3, "right": 3,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"border": "none", "border": "none",
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
@@ -1373,20 +1373,19 @@ exports[`test render renders with filters 1`] = `
"borderBottomRightRadius": "0", "borderBottomRightRadius": "0",
"borderTopRightRadius": "0", "borderTopRightRadius": "0",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"MsHighContrastAdjust": "none", "MsHighContrastAdjust": "none",
"backgroundColor": "Window", "backgroundColor": "Window",
"border": "1px solid WindowText", "border": "1px solid WindowText",
"borderRightWidth": "0", "borderRightWidth": "0",
"color": "WindowText", "color": "WindowText",
"forcedColorAdjust": "none",
}, },
}, },
}, },
".ms-Button--primary + .ms-Button": Object { ".ms-Button--primary + .ms-Button": Object {
"border": "none", "border": "none",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"border": "1px solid WindowText", "border": "1px solid WindowText",
"borderLeftWidth": "0", "borderLeftWidth": "0",
}, },
@@ -1399,11 +1398,10 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"MsHighContrastAdjust": "none", "MsHighContrastAdjust": "none",
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
"color": "Window", "color": "Window",
"forcedColorAdjust": "none",
}, },
}, },
}, },
@@ -1413,11 +1411,10 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"MsHighContrastAdjust": "none", "MsHighContrastAdjust": "none",
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
"color": "Window", "color": "Window",
"forcedColorAdjust": "none",
}, },
}, },
}, },
@@ -1427,11 +1424,12 @@ exports[`test render renders with filters 1`] = `
"border": "none", "border": "none",
"outline": "none", "outline": "none",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"MsHighContrastAdjust": "none",
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
},
"@media screen and (forced-colors: active)": Object {
"forcedColorAdjust": "none", "forcedColorAdjust": "none",
}, },
}, },
@@ -1443,7 +1441,7 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "Highlight", "backgroundColor": "Highlight",
"color": "Window", "color": "Window",
}, },
@@ -1452,7 +1450,7 @@ exports[`test render renders with filters 1`] = `
".ms-Button.is-disabled": Object { ".ms-Button.is-disabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1468,7 +1466,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 31, "right": 31,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
}, },
}, },
@@ -1480,7 +1478,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 31, "right": 31,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "WindowText", "backgroundColor": "WindowText",
}, },
}, },
@@ -1497,7 +1495,7 @@ exports[`test render renders with filters 1`] = `
"position": "absolute", "position": "absolute",
"right": 31, "right": 31,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "GrayText", "backgroundColor": "GrayText",
}, },
}, },
@@ -1520,7 +1518,7 @@ exports[`test render renders with filters 1`] = `
":hover": Object { ":hover": Object {
"backgroundColor": "#edebe9", "backgroundColor": "#edebe9",
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"color": "Highlight", "color": "Highlight",
}, },
}, },
@@ -1528,11 +1526,6 @@ exports[`test render renders with filters 1`] = `
}, },
}, },
Object { Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object {
".ms-Button-menuIcon": Object {
"color": "WindowText",
},
},
"border": "1px solid #8a8886", "border": "1px solid #8a8886",
"borderBottomRightRadius": "2px", "borderBottomRightRadius": "2px",
"borderLeft": "none", "borderLeft": "none",
@@ -1578,7 +1571,7 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
".ms-Button--primary": Object { ".ms-Button--primary": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1587,7 +1580,7 @@ exports[`test render renders with filters 1`] = `
}, },
".ms-Button-menuIcon": Object { ".ms-Button-menuIcon": Object {
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"color": "GrayText", "color": "GrayText",
}, },
}, },
@@ -1595,7 +1588,7 @@ exports[`test render renders with filters 1`] = `
":hover": Object { ":hover": Object {
"cursor": "default", "cursor": "default",
}, },
"@media screen and (-ms-high-contrast: active), (forced-colors: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"backgroundColor": "Window", "backgroundColor": "Window",
"border": "1px solid GrayText", "border": "1px solid GrayText",
"color": "GrayText", "color": "GrayText",
@@ -1900,7 +1893,7 @@ exports[`test render renders with filters 1`] = `
> >
<button <button
aria-disabled={true} aria-disabled={true}
className="ms-Button ms-Button--default is-disabled directoryListButton root-89" className="ms-Button ms-Button--default is-disabled directoryListButton root-54"
data-is-focusable={false} data-is-focusable={false}
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
@@ -1912,7 +1905,7 @@ exports[`test render renders with filters 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-90" className="ms-Button-flexContainer flexContainer-55"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<div <div
@@ -1943,7 +1936,8 @@ exports[`test render renders with filters 1`] = `
</List> </List>
</div> </div>
<div <div
className="stickyBelow-75" aria-hidden="true"
className="stickyBelow-43"
style={ style={
Object { Object {
"bottom": "0px", "bottom": "0px",
@@ -1954,7 +1948,7 @@ exports[`test render renders with filters 1`] = `
} }
> >
<div <div
className="stickyBelowItems-76" className="stickyBelowItems-44"
/> />
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@ import { ChildrenMargin } from "./GitHubStyleConstants";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient"; import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as UrlUtility from "../../../Common/UrlUtility"; import UrlUtility from "../../../Common/UrlUtility";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
export interface AddRepoComponentProps { export interface AddRepoComponentProps {

View File

@@ -18,7 +18,7 @@ import {
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem } from "../../../../Juno/JunoClient"; import { IGalleryItem } from "../../../../Juno/JunoClient";
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil"; import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
export interface GalleryCardComponentProps { export interface GalleryCardComponentProps {
@@ -47,7 +47,6 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
private static readonly cardItemGapBig = 10; private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8; private static readonly cardItemGapSmall = 8;
private static readonly cardDeleteSpinnerHeight = 360; private static readonly cardDeleteSpinnerHeight = 360;
private static readonly smallTextLineHeight = 18;
constructor(props: GalleryCardComponentProps) { constructor(props: GalleryCardComponentProps) {
super(props); super(props);
@@ -104,7 +103,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
</Card.Item> </Card.Item>
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}> <Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
<Text variant="small" nowrap styles={{ root: { height: GalleryCardComponent.smallTextLineHeight } }}> <Text variant="small" nowrap>
{this.props.data.tags ? ( {this.props.data.tags ? (
this.props.data.tags.map((tag, index, array) => ( this.props.data.tags.map((tag, index, array) => (
<span key={tag}> <span key={tag}>
@@ -130,7 +129,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
{cardTitle} {cardTitle}
</Text> </Text>
<Text variant="small" styles={{ root: { height: GalleryCardComponent.smallTextLineHeight * 2 } }}> <Text variant="small" styles={{ root: { height: 36 } }}>
{this.renderTruncatedDescription()} {this.renderTruncatedDescription()}
</Text> </Text>

View File

@@ -50,13 +50,6 @@ exports[`GalleryCardComponent renders 1`] = `
> >
<Text <Text
nowrap={true} nowrap={true}
styles={
Object {
"root": Object {
"height": 18,
},
}
}
variant="small" variant="small"
> >
<span <span
@@ -107,7 +100,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
variant="tiny" variant="tiny"
> >
<Icon <StyledIconBase
iconName="RedEye" iconName="RedEye"
styles={ styles={
Object { Object {
@@ -131,7 +124,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
variant="tiny" variant="tiny"
> >
<Icon <StyledIconBase
iconName="Download" iconName="Download"
styles={ styles={
Object { Object {
@@ -155,7 +148,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
variant="tiny" variant="tiny"
> >
<Icon <StyledIconBase
iconName="Heart" iconName="Heart"
styles={ styles={
Object { Object {
@@ -180,7 +173,7 @@ exports[`GalleryCardComponent renders 1`] = `
} }
} }
> >
<Separator <Styled
styles={ styles={
Object { Object {
"root": Object { "root": Object {

View File

@@ -13,7 +13,7 @@ exports[`InfoComponent renders 1`] = `
<div <div
className="infoPanelMain" className="infoPanelMain"
> >
<Icon <StyledIconBase
className="infoIconMain" className="infoIconMain"
iconName="Help" iconName="Help"
styles={ styles={

View File

@@ -14,7 +14,7 @@ import {
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem } from "../../../Juno/JunoClient"; import { IGalleryItem } from "../../../Juno/JunoClient";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg"; import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent"; import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";

View File

@@ -68,14 +68,14 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
Invalid Date Invalid Date
</Text> </Text>
<Text> <Text>
<Icon <StyledIconBase
iconName="RedEye" iconName="RedEye"
/> />
0 0
</Text> </Text>
<Text> <Text>
<Icon <StyledIconBase
iconName="Download" iconName="Download"
/> />
0 0
@@ -180,14 +180,14 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
Invalid Date Invalid Date
</Text> </Text>
<Text> <Text>
<Icon <StyledIconBase
iconName="RedEye" iconName="RedEye"
/> />
0 0
</Text> </Text>
<Text> <Text>
<Icon <StyledIconBase
iconName="Download" iconName="Download"
/> />
0 0

View File

@@ -1,19 +1,17 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import ko from "knockout";
import React from "react"; import React from "react";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
import { isDirty, TtlType } from "./SettingsUtils";
import { collection } from "./TestUtils"; import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout";
import { TtlType, isDirty } from "./SettingsUtils";
import Explorer from "../../Explorer";
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({ jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined), getIndexTransformationProgress: jest.fn().mockReturnValue(undefined),
})); }));
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
jest.mock("../../../Common/dataAccess/updateCollection", () => ({ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
updateCollection: jest.fn().mockReturnValue({ updateCollection: jest.fn().mockReturnValue({
id: undefined, id: undefined,
@@ -31,6 +29,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
analyticalStorageTtl: undefined, analyticalStorageTtl: undefined,
} as MongoDBCollectionResource), } as MongoDBCollectionResource),
})); }));
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({ jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
})); }));
@@ -134,6 +134,7 @@ describe("SettingsComponent", () => {
loadCollections: undefined, loadCollections: undefined,
findCollectionWithId: undefined, findCollectionWithId: undefined,
openAddCollection: undefined, openAddCollection: undefined,
onDeleteDatabaseContextMenuClick: undefined,
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,

View File

@@ -1,51 +1,49 @@
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants"; 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 { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
ConflictResolutionComponent,
ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { import {
MongoIndexingPolicyComponent, MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps, MongoIndexingPolicyComponentProps,
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent"; } from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import { import {
AddMongoIndexProps,
ChangeFeedPolicyState,
GeospatialConfigType,
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput, hasDatabaseSharedThroughput,
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
SettingsV2TabTypes,
getTabTitle,
isDirty, isDirty,
AddMongoIndexProps,
MongoIndexTypes, MongoIndexTypes,
parseConflictResolutionMode, parseConflictResolutionMode,
parseConflictResolutionProcedure, parseConflictResolutionProcedure,
SettingsV2TabTypes, getMongoNotification,
TtlType,
} from "./SettingsUtils"; } from "./SettingsUtils";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
import "./SettingsComponent.less";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
interface SettingsV2TabInfo { interface SettingsV2TabInfo {
tab: SettingsV2TabTypes; tab: SettingsV2TabTypes;
@@ -139,7 +137,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowIndexingPolicyEditor = this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB(); this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy
);
// Mongo container with system partition key still treat as "Fixed" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
@@ -325,6 +325,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
? this.saveCollectionSettings(startKey) ? this.saveCollectionSettings(startKey)
: this.saveDatabaseSettings(startKey)); : this.saveDatabaseSettings(startKey));
} catch (error) { } catch (error) {
this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true); this.props.settingsTab.isExecutionError(true);
console.error(error); console.error(error);
traceFailure( traceFailure(
@@ -698,6 +699,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
} }
this.container.isRefreshingExplorer(false);
this.setBaseline(); this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected }); this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess( traceSuccess(
@@ -860,6 +862,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}); });
} }
} }
this.container.isRefreshingExplorer(false);
this.setBaseline(); this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected }); this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess( traceSuccess(
@@ -874,18 +877,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
); );
}; };
public getMongoIndexTabContent = (
mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps
): JSX.Element => {
if (userContext.authType === AuthType.AAD) {
if (this.container.isEnableMongoCapabilityPresent()) {
return <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />;
}
return undefined;
}
return mongoIndexingPolicyAADError;
};
public render(): JSX.Element { public render(): JSX.Element {
const scaleComponentProps: ScaleComponentProps = { const scaleComponentProps: ScaleComponentProps = {
collection: this.collection, collection: this.collection,
@@ -1003,11 +994,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />, content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />,
}); });
} else if (this.container.isPreferredApiMongoDB()) { } else if (this.container.isPreferredApiMongoDB()) {
const mongoIndexTabContext = this.getMongoIndexTabContent(mongoIndexingPolicyComponentProps); if (this.container.isEnableMongoCapabilityPresent()) {
if (mongoIndexTabContext) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab, tab: SettingsV2TabTypes.IndexingPolicyTab,
content: mongoIndexTabContext, content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />,
});
} else {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: mongoIndexingPolicyAADError,
}); });
} }
} }

View File

@@ -23,6 +23,7 @@ import {
ITextStyles, ITextStyles,
IDetailsRowStyles, IDetailsRowStyles,
IStackStyles, IStackStyles,
IIconStyles,
IDetailsListStyles, IDetailsListStyles,
IDropdownStyles, IDropdownStyles,
ISeparatorStyles, ISeparatorStyles,
@@ -115,6 +116,8 @@ export const addMongoIndexSubElementsTokens: IStackTokens = {
childrenGap: 20, childrenGap: 20,
}; };
export const accordionIconStyles: IIconStyles = { root: { paddingTop: 7 } };
export const mediumWidthStackStyles: IStackStyles = { root: { width: 600 } }; export const mediumWidthStackStyles: IStackStyles = { root: { width: 600 } };
export const shortWidthTextFieldStyles: Partial<ITextFieldStyles> = { root: { paddingLeft: 10, width: 210 } }; export const shortWidthTextFieldStyles: Partial<ITextFieldStyles> = { root: { paddingLeft: 10, width: 210 } };

View File

@@ -239,7 +239,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return ( return (
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}> <Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}> <CollapsibleSectionComponent title="Current index(es)">
{ {
<> <>
<DetailsList <DetailsList
@@ -266,7 +266,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return ( return (
<Stack styles={mediumWidthStackStyles}> <Stack styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Index(es) to be dropped" isExpandedByDefault={true}> <CollapsibleSectionComponent title="Index(es) to be dropped">
{indexesToBeDropped.length > 0 && ( {indexesToBeDropped.length > 0 && (
<DetailsList <DetailsList
styles={customDetailsListStyles} styles={customDetailsListStyles}

View File

@@ -42,7 +42,6 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
} }
> >
<CollapsibleSectionComponent <CollapsibleSectionComponent
isExpandedByDefault={true}
title="Current index(es)" title="Current index(es)"
> >
<StyledWithViewportComponent <StyledWithViewportComponent
@@ -115,7 +114,7 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
</Stack> </Stack>
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
</Stack> </Stack>
<Separator <Styled
styles={ styles={
Object { Object {
"root": Array [ "root": Array [
@@ -140,7 +139,6 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
} }
> >
<CollapsibleSectionComponent <CollapsibleSectionComponent
isExpandedByDefault={true}
title="Index(es) to be dropped" title="Index(es) to be dropped"
/> />
</Stack> </Stack>

View File

@@ -1,24 +1,23 @@
import { Label, Link, MessageBar, MessageBarType, Stack, Text, TextField } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import * as Constants from "../../../../Common/Constants"; import * as Constants from "../../../../Common/Constants";
import { configContext, Platform } from "../../../../ConfigContext"; import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../Shared/Constants"; import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import { import {
getTextFieldStyles, getTextFieldStyles,
subComponentStackProps,
titleAndInputStackProps,
throughputUnit,
getThroughputApplyLongDelayMessage, getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage, getThroughputApplyShortDelayMessage,
subComponentStackProps,
throughputUnit,
titleAndInputStackProps,
updateThroughputBeyondLimitWarningMessage, updateThroughputBeyondLimitWarningMessage,
} from "../SettingsRenderUtils"; } from "../SettingsRenderUtils";
import { hasDatabaseSharedThroughput } from "../SettingsUtils"; import { hasDatabaseSharedThroughput } from "../SettingsUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
import { configContext, Platform } from "../../../../ConfigContext";
export interface ScaleComponentProps { export interface ScaleComponentProps {
collection: ViewModels.Collection; collection: ViewModels.Collection;
@@ -80,7 +79,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
}; };
public getMaxRUs = (): number => { public getMaxRUs = (): number => {
if (userContext.isTryCosmosDBSubscription) { if (this.props.container?.isTryCosmosDBSubscription()) {
return Constants.TryCosmosExperience.maxRU; return Constants.TryCosmosExperience.maxRU;
} }
@@ -92,7 +91,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
}; };
public getMinRUs = (): number => { public getMinRUs = (): number => {
if (userContext.isTryCosmosDBSubscription) { if (this.props.container?.isTryCosmosDBSubscription()) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400; return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
} }
@@ -173,6 +172,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
databaseAccount={this.props.container.databaseAccount()} databaseAccount={this.props.container.databaseAccount()}
databaseName={this.databaseId} databaseName={this.databaseId}
collectionName={this.collectionId} collectionName={this.collectionId}
serverId={this.props.container.serverId()}
throughput={this.props.throughput} throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline} throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange} onThroughputChange={this.props.onThroughputChange}

View File

@@ -1,16 +1,17 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels";
import { import {
ThroughputInputAutoPilotV3Component, ThroughputInputAutoPilotV3Component,
ThroughputInputAutoPilotV3Props, ThroughputInputAutoPilotV3Props,
} from "./ThroughputInputAutoPilotV3Component"; } from "./ThroughputInputAutoPilotV3Component";
import * as DataModels from "../../../../../Contracts/DataModels";
describe("ThroughputInputAutoPilotV3Component", () => { describe("ThroughputInputAutoPilotV3Component", () => {
const baseProps: ThroughputInputAutoPilotV3Props = { const baseProps: ThroughputInputAutoPilotV3Props = {
databaseAccount: {} as DataModels.DatabaseAccount, databaseAccount: {} as DataModels.DatabaseAccount,
databaseName: "test", databaseName: "test",
collectionName: "test", collectionName: "test",
serverId: undefined,
wasAutopilotOriginallySet: false, wasAutopilotOriginallySet: false,
throughput: 100, throughput: 100,
throughputBaseline: 100, throughputBaseline: 100,

View File

@@ -1,52 +1,55 @@
import React from "react";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { import {
Checkbox, getTextFieldStyles,
getToolTipContainer,
noLeftPaddingCheckBoxStyle,
titleAndInputStackProps,
checkBoxAndInputStackProps,
getChoiceGroupStyles,
messageBarStyles,
getEstimatedSpendingElement,
getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement,
saveThroughputWarningMessage,
ManualEstimatedSpendingDisplayProps,
AutoscaleEstimatedSpendingDisplayProps,
PriceBreakdown,
getRuPriceBreakdown,
transparentDetailsHeaderStyle,
} from "../../SettingsRenderUtils";
import {
Text,
TextField,
ChoiceGroup, ChoiceGroup,
FontIcon,
IChoiceGroupOption, IChoiceGroupOption,
IColumn, Checkbox,
Stack,
Label, Label,
Link, Link,
MessageBar, MessageBar,
Stack, FontIcon,
Text, IColumn,
TextField,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
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 { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
AutoscaleEstimatedSpendingDisplayProps,
checkBoxAndInputStackProps,
getAutoPilotV3SpendElement,
getChoiceGroupStyles,
getEstimatedSpendingElement,
getRuPriceBreakdown,
getTextFieldStyles,
getToolTipContainer,
ManualEstimatedSpendingDisplayProps,
manualToAutoscaleDisclaimerElement,
messageBarStyles,
noLeftPaddingCheckBoxStyle,
PriceBreakdown,
saveThroughputWarningMessage,
titleAndInputStackProps,
transparentDetailsHeaderStyle,
} from "../../SettingsRenderUtils";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent"; import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import * as SharedConstants from "../../../../../Shared/Constants";
import * as DataModels from "../../../../../Contracts/DataModels";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { userContext } from "../../../../../UserContext";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
import { Features } from "../../../../../Common/Constants";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
export interface ThroughputInputAutoPilotV3Props { export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount; databaseAccount: DataModels.DatabaseAccount;
databaseName: string; databaseName: string;
collectionName: string; collectionName: string;
serverId: string;
throughput: number; throughput: number;
throughputBaseline: number; throughputBaseline: number;
onThroughputChange: (newThroughput: number) => void; onThroughputChange: (newThroughput: number) => void;
@@ -179,6 +182,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
} }
const isDirty: boolean = this.IsComponentDirty().isDiscardable; const isDirty: boolean = this.IsComponentDirty().isDiscardable;
const serverId: string = this.props.serverId;
const regions = account?.properties?.readLocations?.length || 1; const regions = account?.properties?.readLocations?.length || 1;
const multimaster = account?.properties?.enableMultipleWriteLocations || false; const multimaster = account?.properties?.enableMultipleWriteLocations || false;
@@ -188,7 +192,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
estimatedSpend = this.getEstimatedManualSpendElement( estimatedSpend = this.getEstimatedManualSpendElement(
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set... // if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline, this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
isDirty ? this.props.throughput : undefined isDirty ? this.props.throughput : undefined
@@ -196,7 +200,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
} else { } else {
estimatedSpend = this.getEstimatedAutoscaleSpendElement( estimatedSpend = this.getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughputBaseline, this.props.maxAutoPilotThroughputBaseline,
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
isDirty ? this.props.maxAutoPilotThroughput : undefined isDirty ? this.props.maxAutoPilotThroughput : undefined
@@ -464,7 +468,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`; const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
const oneTBinKB = 1000000000; const oneTBinKB = 1000000000;
const minRUperGB = 10; const minRUperGB = 10;
const featureFlagEnabled = userContext.features.showMinRUSurvey; const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey);
const collectionIsEligible = const collectionIsEligible =
userContext.subscriptionType !== SubscriptionType.Internal && userContext.subscriptionType !== SubscriptionType.Internal &&
this.props.usageSizeInKB > oneTBinKB && this.props.usageSizeInKB > oneTBinKB &&

View File

@@ -41,7 +41,7 @@ exports[`ToolTipLabelComponent renders 1`] = `
} }
} }
> >
<Icon <StyledIconBase
ariaLabel="Info" ariaLabel="Info"
iconName="Info" iconName="Info"
styles={ styles={

View File

@@ -1,23 +1,23 @@
import ko from "knockout"; import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { import {
getMongoIndexType, getMongoIndexType,
getMongoIndexTypeText,
getMongoNotification, getMongoNotification,
getSanitizedInputValue, getSanitizedInputValue,
hasDatabaseSharedThroughput, hasDatabaseSharedThroughput,
isDirty, isDirty,
isIndexTransforming,
MongoIndexTypes, MongoIndexTypes,
MongoNotificationType, MongoNotificationType,
MongoWildcardPlaceHolder,
parseConflictResolutionMode, parseConflictResolutionMode,
parseConflictResolutionProcedure, parseConflictResolutionProcedure,
MongoWildcardPlaceHolder,
getMongoIndexTypeText,
SingleFieldText, SingleFieldText,
WildcardText, WildcardText,
isIndexTransforming,
} from "./SettingsUtils"; } from "./SettingsUtils";
import { collection } from "./TestUtils"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import ko from "knockout";
describe("SettingsUtils", () => { describe("SettingsUtils", () => {
it("hasDatabaseSharedThroughput", () => { it("hasDatabaseSharedThroughput", () => {
@@ -42,6 +42,7 @@ describe("SettingsUtils", () => {
loadCollections: undefined, loadCollections: undefined,
findCollectionWithId: undefined, findCollectionWithId: undefined,
openAddCollection: undefined, openAddCollection: undefined,
onDeleteDatabaseContextMenuClick: undefined,
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,

View File

@@ -1,7 +1,7 @@
import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { DescriptionType, NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes"; import { shallow } from "enzyme";
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: SmartUiDescriptor = {
@@ -97,9 +97,9 @@ describe("SmartUiComponent", () => {
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
{ labelTKey: "Database 1", key: "db1" }, { label: "Database 1", key: "db1" },
{ labelTKey: "Database 2", key: "db2" }, { label: "Database 2", key: "db2" },
{ labelTKey: "Database 3", key: "db3" }, { label: "Database 3", key: "db3" },
], ],
defaultKey: "db2", defaultKey: "db2",
}, },

View File

@@ -1,13 +1,14 @@
import { TFunction } from "i18next"; import * as React from "react";
import { Label, Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react"; import { Position } from "office-ui-fabric-react/lib/utilities/positioning";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { Slider } from "office-ui-fabric-react/lib/Slider"; import { Slider } from "office-ui-fabric-react/lib/Slider";
import { SpinButton } from "office-ui-fabric-react/lib/SpinButton"; import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
import { IStackTokens, Stack } from "office-ui-fabric-react/lib/Stack"; import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { Text } from "office-ui-fabric-react/lib/Text";
import { TextField } from "office-ui-fabric-react/lib/TextField"; import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Position } from "office-ui-fabric-react/lib/utilities/positioning"; import { Text } from "office-ui-fabric-react/lib/Text";
import * as React from "react"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
import { Label, Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less";
import { import {
ChoiceItem, ChoiceItem,
Description, Description,
@@ -18,9 +19,8 @@ import {
NumberUiType, NumberUiType,
SmartUiInput, SmartUiInput,
} from "../../../SelfServe/SelfServeTypes"; } from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next";
import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent"; import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent";
import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less";
/** /**
* Generic UX renderer * Generic UX renderer
@@ -138,12 +138,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
); );
} }
private renderTextInput(input: StringInput, labelId: string, labelElement: JSX.Element): JSX.Element { private renderTextInput(input: StringInput, labelId: string): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as string; const value = this.props.currentValues.get(input.dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<Stack> <div className="stringInputContainer">
{labelElement}
<TextField <TextField
id={`${input.dataFieldName}-textField-input`} id={`${input.dataFieldName}-textField-input`}
aria-labelledby={labelId} aria-labelledby={labelId}
@@ -156,32 +155,25 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
root: { width: 400 }, root: { width: 400 },
}} }}
/> />
</Stack> </div>
); );
} }
private renderDescription(input: DescriptionDisplay, labelId: string, labelElement: JSX.Element): JSX.Element { private renderDescription(input: DescriptionDisplay, labelId: string): JSX.Element {
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description); const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description);
if (!description) { if (!description) {
if (!input.isDynamicDescription) { return this.renderError("Description is not provided.");
return this.renderError("Description is not provided.");
}
// If input is a dynamic description and description is not available, empty element is rendered
return <></>;
} }
const descriptionElement = ( const descriptionElement = (
<Stack> <Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
{labelElement} {this.props.getTranslation(description.textTKey)}{" "}
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}> {description.link && (
{this.props.getTranslation(description.textTKey)}{" "} <Link target="_blank" href={description.link.href}>
{description.link && ( {this.props.getTranslation(description.link.textTKey)}
<Link target="_blank" href={description.link.href}> </Link>
{this.props.getTranslation(description.link.textTKey)} )}
</Link> </Text>
)}
</Text>
</Stack>
); );
if (description.type === DescriptionType.Text) { if (description.type === DescriptionType.Text) {
@@ -235,7 +227,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined; return undefined;
}; };
private renderNumberInput(input: NumberInput, labelId: string, labelElement: JSX.Element): JSX.Element { private renderNumberInput(input: NumberInput, labelId: string): JSX.Element {
const { labelTKey, min, max, dataFieldName, step } = input; const { labelTKey, min, max, dataFieldName, step } = input;
const props = { const props = {
min: min, min: min,
@@ -248,72 +240,61 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
if (input.uiType === NumberUiType.Spinner) { if (input.uiType === NumberUiType.Spinner) {
return ( return (
<Stack> <Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}>
{labelElement} <SpinButton
<Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}> {...props}
<SpinButton id={`${input.dataFieldName}-spinner-input`}
{...props} value={value?.toString()}
id={`${input.dataFieldName}-spinner-input`} onValidate={(newValue) => this.onValidate(input, newValue, props.min, props.max)}
value={value?.toString()} onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
onValidate={(newValue) => this.onValidate(input, newValue, props.min, props.max)} onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)} labelPosition={Position.top}
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)} aria-labelledby={labelId}
labelPosition={Position.top} disabled={disabled}
aria-labelledby={labelId} />
disabled={disabled} {this.state.errors.has(dataFieldName) && (
/> <MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
{this.state.errors.has(dataFieldName) && ( )}
<MessageBar messageBarType={MessageBarType.error}>
Error: {this.state.errors.get(dataFieldName)}
</MessageBar>
)}
</Stack>
</Stack> </Stack>
); );
} else if (input.uiType === NumberUiType.Slider) { } else if (input.uiType === NumberUiType.Slider) {
return ( return (
<Stack> <div id={`${input.dataFieldName}-slider-input`}>
{labelElement} <Slider
<div id={`${input.dataFieldName}-slider-input`}> {...props}
<Slider value={value}
{...props} disabled={disabled}
value={value} onChange={(newValue) => this.props.onInputChange(input, newValue)}
disabled={disabled} styles={{
onChange={(newValue) => this.props.onInputChange(input, newValue)} root: { width: 400 },
styles={{ valueLabel: SmartUiComponent.labelStyle,
root: { width: 400 }, }}
valueLabel: SmartUiComponent.labelStyle, />
}} </div>
/>
</div>
</Stack>
); );
} else { } else {
return <>Unsupported number UI type {input.uiType}</>; return <>Unsupported number UI type {input.uiType}</>;
} }
} }
private renderBooleanInput(input: BooleanInput, labelId: string, labelElement: JSX.Element): JSX.Element { private renderBooleanInput(input: BooleanInput, labelId: string): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean; const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<Stack> <Toggle
{labelElement} id={`${input.dataFieldName}-toggle-input`}
<Toggle aria-labelledby={labelId}
id={`${input.dataFieldName}-toggle-input`} checked={value || false}
aria-labelledby={labelId} onText={this.props.getTranslation(input.trueLabelTKey)}
checked={value || false} offText={this.props.getTranslation(input.falseLabelTKey)}
onText={this.props.getTranslation(input.trueLabelTKey)} disabled={disabled}
offText={this.props.getTranslation(input.falseLabelTKey)} onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
disabled={disabled} styles={{ root: { width: 400 } }}
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)} />
styles={{ root: { width: 400 } }}
/>
</Stack>
); );
} }
private renderChoiceInput(input: ChoiceInput, labelId: string, labelElement: JSX.Element): JSX.Element { private renderChoiceInput(input: ChoiceInput, labelId: string): JSX.Element {
const { defaultKey, dataFieldName, choices, placeholderTKey } = input; const { defaultKey, dataFieldName, choices, placeholderTKey } = input;
const value = this.props.currentValues.get(dataFieldName)?.value as string; const value = this.props.currentValues.get(dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
@@ -322,26 +303,22 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
selectedKey = ""; selectedKey = "";
} }
return ( return (
<Stack> <Dropdown
{labelElement} id={`${input.dataFieldName}-dropdown-input`}
<Dropdown aria-labelledby={labelId}
id={`${input.dataFieldName}-dropdown-input`} selectedKey={selectedKey}
aria-labelledby={labelId} onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
selectedKey={selectedKey} placeholder={this.props.getTranslation(placeholderTKey)}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())} disabled={disabled}
placeholder={this.props.getTranslation(placeholderTKey)} options={choices.map((c) => ({
disabled={disabled} key: c.key,
// Removed dropdownWidth="auto" as dropdown accept only number text: this.props.getTranslation(c.label),
options={choices.map((c) => ({ }))}
key: c.key, styles={{
text: this.props.getTranslation(c.labelTKey), root: { width: 400 },
}))} dropdown: SmartUiComponent.labelStyle,
styles={{ }}
root: { width: 400 }, />
dropdown: SmartUiComponent.labelStyle,
}}
/>
</Stack>
); );
} }
@@ -349,7 +326,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>; return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>;
} }
private renderElement(input: AnyDisplay, info: Info): JSX.Element { private renderDisplayWithInfoBubble(input: AnyDisplay, info: Info): JSX.Element {
if (input.errorMessage) { if (input.errorMessage) {
return this.renderError(input.errorMessage); return this.renderError(input.errorMessage);
} }
@@ -358,31 +335,34 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return <></>; return <></>;
} }
const labelId = `${input.dataFieldName}-label`; const labelId = `${input.dataFieldName}-label`;
const labelElement: JSX.Element = input.labelTKey && ( return (
<Label id={labelId}> <Stack>
<ToolTipLabelComponent {input.labelTKey && (
label={this.props.getTranslation(input.labelTKey)} <Label id={labelId}>
toolTipElement={this.renderInfo(info)} <ToolTipLabelComponent
/> label={this.props.getTranslation(input.labelTKey)}
</Label> toolTipElement={this.renderInfo(info)}
/>
</Label>
)}
{this.renderDisplay(input, labelId)}
</Stack>
); );
return <Stack>{this.renderControl(input, labelId, labelElement)}</Stack>;
} }
private renderControl(input: AnyDisplay, labelId: string, labelElement: JSX.Element): JSX.Element { private renderDisplay(input: AnyDisplay, labelId: string): JSX.Element {
switch (input.type) { switch (input.type) {
case "string": case "string":
if ("description" in input || "isDynamicDescription" in input) { if ("description" in input || "isDynamicDescription" in input) {
return this.renderDescription(input as DescriptionDisplay, labelId, labelElement); return this.renderDescription(input as DescriptionDisplay, labelId);
} }
return this.renderTextInput(input as StringInput, labelId, labelElement); return this.renderTextInput(input as StringInput, labelId);
case "number": case "number":
return this.renderNumberInput(input as NumberInput, labelId, labelElement); return this.renderNumberInput(input as NumberInput, labelId);
case "boolean": case "boolean":
return this.renderBooleanInput(input as BooleanInput, labelId, labelElement); return this.renderBooleanInput(input as BooleanInput, labelId);
case "object": case "object":
return this.renderChoiceInput(input as ChoiceInput, labelId, labelElement); return this.renderChoiceInput(input as ChoiceInput, labelId);
default: default:
throw new Error(`Unknown input type: ${input.type}`); throw new Error(`Unknown input type: ${input.type}`);
} }
@@ -393,7 +373,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item>{node.input && this.renderElement(node.input, node.info as Info)}</Stack.Item> <Stack.Item>{node.input && this.renderDisplayWithInfoBubble(node.input, node.info as Info)}</Stack.Item>
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)} {node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack> </Stack>
); );

View File

@@ -23,21 +23,19 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <Text
<Text aria-labelledby="description-label"
aria-labelledby="description-label" id="description-text-display"
id="description-text-display" >
this is an example description text.
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
> >
this is an example description text. Click here for more information.
</StyledLinkBase>
<StyledLinkBase </Text>
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -55,53 +53,51 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="throughput-label"
id="throughput-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Throughput (input)"
label="Throughput (input)" />
/> </StyledLabelBase>
</StyledLabelBase> <Stack
<Stack styles={
styles={ Object {
"root": Object {
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object { Object {
"root": Object { "iconName": "ChevronDownSmall",
"width": 400,
},
} }
} }
tokens={ disabled={true}
id="throughput-spinner-input"
incrementButtonIcon={
Object { Object {
"childrenGap": 2, "iconName": "ChevronUpSmall",
} }
} }
> label=""
<CustomizedSpinButton labelPosition={0}
aria-labelledby="throughput-label" max={500}
ariaLabel="Throughput (input)" min={400}
decrementButtonIcon={ onDecrement={[Function]}
Object { onIncrement={[Function]}
"iconName": "ChevronDownSmall", onValidate={[Function]}
} step={10}
} />
disabled={true}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label=""
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
@@ -120,39 +116,37 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="throughput2-label"
id="throughput2-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Throughput (Slider)"
label="Throughput (Slider)" />
/> </StyledLabelBase>
</StyledLabelBase> <div
<div id="throughput2-slider-input"
id="throughput2-slider-input" >
> <StyledSliderBase
<StyledSliderBase ariaLabel="Throughput (Slider)"
ariaLabel="Throughput (Slider)" disabled={true}
disabled={true} max={500}
max={500} min={400}
min={400} onChange={[Function]}
onChange={[Function]} step={10}
step={10} styles={
styles={ Object {
Object { "root": Object {
"root": Object { "width": 400,
"width": 400, },
}, "valueLabel": Object {
"valueLabel": Object { "color": "#393939",
"color": "#393939", "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "fontSize": 12,
"fontSize": 12, },
},
}
} }
/> }
</div> />
</Stack> </div>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -191,14 +185,16 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="containerId-label"
id="containerId-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Container id"
label="Container id" />
/> </StyledLabelBase>
</StyledLabelBase> <div
className="stringInputContainer"
>
<StyledTextFieldBase <StyledTextFieldBase
aria-labelledby="containerId-label" aria-labelledby="containerId-label"
disabled={true} disabled={true}
@@ -214,7 +210,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
type="text" type="text"
value="" value=""
/> />
</Stack> </div>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -232,31 +228,29 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="analyticalStore-label"
id="analyticalStore-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Analytical Store"
label="Analytical Store"
/>
</StyledLabelBase>
<StyledToggleBase
aria-labelledby="analyticalStore-label"
checked={false}
disabled={true}
id="analyticalStore-toggle-input"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/> />
</Stack> </StyledLabelBase>
<StyledToggleBase
aria-labelledby="analyticalStore-label"
checked={false}
disabled={true}
id="analyticalStore-toggle-input"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -274,50 +268,48 @@ exports[`SmartUiComponent disable all inputs 1`] = `
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="database-label"
id="database-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Database"
label="Database"
/>
</StyledLabelBase>
<StyledWithResponsiveMode
aria-labelledby="database-label"
disabled={true}
id="database-dropdown-input"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/> />
</Stack> </StyledLabelBase>
<StyledWithResponsiveMode
aria-labelledby="database-label"
disabled={true}
id="database-dropdown-input"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -348,21 +340,19 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <Text
<Text aria-labelledby="description-label"
aria-labelledby="description-label" id="description-text-display"
id="description-text-display" >
this is an example description text.
<StyledLinkBase
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
> >
this is an example description text. Click here for more information.
</StyledLinkBase>
<StyledLinkBase </Text>
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
target="_blank"
>
Click here for more information.
</StyledLinkBase>
</Text>
</Stack>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -380,53 +370,51 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="throughput-label"
id="throughput-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Throughput (input)"
label="Throughput (input)" />
/> </StyledLabelBase>
</StyledLabelBase> <Stack
<Stack styles={
styles={ Object {
"root": Object {
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 2,
}
}
>
<CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object { Object {
"root": Object { "iconName": "ChevronDownSmall",
"width": 400,
},
} }
} }
tokens={ disabled={false}
id="throughput-spinner-input"
incrementButtonIcon={
Object { Object {
"childrenGap": 2, "iconName": "ChevronUpSmall",
} }
} }
> label=""
<CustomizedSpinButton labelPosition={0}
aria-labelledby="throughput-label" max={500}
ariaLabel="Throughput (input)" min={400}
decrementButtonIcon={ onDecrement={[Function]}
Object { onIncrement={[Function]}
"iconName": "ChevronDownSmall", onValidate={[Function]}
} step={10}
} />
disabled={false}
id="throughput-spinner-input"
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label=""
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
/>
</Stack>
</Stack> </Stack>
</Stack> </Stack>
</StackItem> </StackItem>
@@ -445,38 +433,36 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="throughput2-label"
id="throughput2-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Throughput (Slider)"
label="Throughput (Slider)" />
/> </StyledLabelBase>
</StyledLabelBase> <div
<div id="throughput2-slider-input"
id="throughput2-slider-input" >
> <StyledSliderBase
<StyledSliderBase ariaLabel="Throughput (Slider)"
ariaLabel="Throughput (Slider)" max={500}
max={500} min={400}
min={400} onChange={[Function]}
onChange={[Function]} step={10}
step={10} styles={
styles={ Object {
Object { "root": Object {
"root": Object { "width": 400,
"width": 400, },
}, "valueLabel": Object {
"valueLabel": Object { "color": "#393939",
"color": "#393939", "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "fontSize": 12,
"fontSize": 12, },
},
}
} }
/> }
</div> />
</Stack> </div>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -515,14 +501,16 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="containerId-label"
id="containerId-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Container id"
label="Container id" />
/> </StyledLabelBase>
</StyledLabelBase> <div
className="stringInputContainer"
>
<StyledTextFieldBase <StyledTextFieldBase
aria-labelledby="containerId-label" aria-labelledby="containerId-label"
id="containerId-textField-input" id="containerId-textField-input"
@@ -537,7 +525,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
type="text" type="text"
value="" value=""
/> />
</Stack> </div>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -555,30 +543,28 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="analyticalStore-label"
id="analyticalStore-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Analytical Store"
label="Analytical Store"
/>
</StyledLabelBase>
<StyledToggleBase
aria-labelledby="analyticalStore-label"
checked={false}
id="analyticalStore-toggle-input"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/> />
</Stack> </StyledLabelBase>
<StyledToggleBase
aria-labelledby="analyticalStore-label"
checked={false}
id="analyticalStore-toggle-input"
offText="Disabled"
onChange={[Function]}
onText="Enabled"
styles={
Object {
"root": Object {
"width": 400,
},
}
}
/>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>
@@ -596,49 +582,47 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
> >
<StackItem> <StackItem>
<Stack> <Stack>
<Stack> <StyledLabelBase
<StyledLabelBase id="database-label"
id="database-label" >
> <ToolTipLabelComponent
<ToolTipLabelComponent label="Database"
label="Database"
/>
</StyledLabelBase>
<StyledWithResponsiveMode
aria-labelledby="database-label"
id="database-dropdown-input"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/> />
</Stack> </StyledLabelBase>
<StyledWithResponsiveMode
aria-labelledby="database-label"
id="database-dropdown-input"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"root": Object {
"width": 400,
},
}
}
/>
</Stack> </Stack>
</StackItem> </StackItem>
</Stack> </Stack>

View File

@@ -1,20 +0,0 @@
@import "../../../../less/Common/Constants";
.throughputInputContainer {
.throughputInputRadioBtn {
margin: 0;
}
}
.throughputInputRadioBtnLabel {
font-size: @mediumFontSize;
padding: 0 @LargeSpace 0 @SmallSpace;
}
.throughputInputSpacing {
margin-bottom: @SmallSpace;
& > * {
margin-bottom: @SmallSpace;
}
}

View File

@@ -1,302 +0,0 @@
import { Checkbox, DirectionalHint, Icon, Link, Stack, Text, TextField, TooltipHost } from "office-ui-fabric-react";
import React from "react";
import * as Constants from "../../../Common/Constants";
import * as SharedConstants from "../../../Shared/Constants";
import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
export interface ThroughputInputProps {
isDatabase: boolean;
showFreeTierExceedThroughputTooltip: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
}
export interface ThroughputInputState {
isAutoscaleSelected: boolean;
throughput: number;
isCostAcknowledged: boolean;
}
export class ThroughputInput extends React.Component<ThroughputInputProps, ThroughputInputState> {
constructor(props: ThroughputInputProps) {
super(props);
this.state = {
isAutoscaleSelected: true,
throughput: AutoPilotUtils.minAutoPilotThroughput,
isCostAcknowledged: false,
};
this.props.setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
this.props.setIsAutoscale(true);
}
render(): JSX.Element {
return (
<div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text variant="small" style={{ lineHeight: "20px" }}>
{this.getThroughputLabelText()}
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={PricingUtils.getRuToolTipText()}>
<Icon iconName="InfoSolid" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
checked={this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onAutoscaleRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Autoscale</span>
<input
className="throughputInputRadioBtn"
aria-label="Manual mode"
checked={!this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onManualRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Manual</span>
</Stack>
{this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Provision maximum RU/s required by this resource. Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px" }}>
Max RU/s
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={this.getAutoScaleTooltip()}>
<Icon iconName="InfoSolid" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.minAutoPilotThroughput}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
required={true}
/>
<Text variant="small">
Your {this.props.isDatabase ? "database" : "container"} throughput will automatically scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.state.throughput)} RU/s (10% of max RU/s) -{" "}
{this.state.throughput} RU/s
</b>{" "}
based on usage.
</Text>
</Stack>
)}
{!this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
this.props.showFreeTierExceedThroughputTooltip &&
this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs400
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: undefined
}
>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={100}
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
required={true}
/>
</TooltipHost>
</Stack>
)}
<CostEstimateText requestUnits={this.state.throughput} isAutoscale={this.state.isAutoscaleSelected} />
{this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<Checkbox
checked={this.state.isCostAcknowledged}
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, margin: "4px 4px 0 0" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
this.setState({ isCostAcknowledged: isChecked });
this.props.onCostAcknowledgeChange(isChecked);
}}
/>
<Text variant="small" style={{ lineHeight: "20px" }}>
{this.getCostAcknowledgeText()}
</Text>
</Stack>
)}
</div>
);
}
private getThroughputLabelText(): string {
if (this.state.isAutoscaleSelected) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
const maxRU: string = userContext.isTryCosmosDBSubscription
? Constants.TryCosmosExperience.maxRU.toLocaleString()
: "unlimited";
return this.state.isAutoscaleSelected
? AutoPilotUtils.getAutoPilotHeaderText()
: `Throughput (${minRU} - ${maxRU} RU/s)`;
}
private onThroughputValueChange(newInput: string): void {
const newThroughput = parseInt(newInput);
this.setState({ throughput: newThroughput });
this.props.setThroughputValue(newThroughput);
}
private getAutoScaleTooltip(): string {
return `After the first ${AutoPilotUtils.getStorageBasedOnUserInput(
this.state.throughput
)} GB of data stored, the max
RU/s will be automatically upgraded based on the new storage value.`;
}
private getCostAcknowledgeText(): string {
const databaseAccount = userContext.databaseAccount;
if (!databaseAccount || !databaseAccount.properties) {
return "";
}
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
return PricingUtils.getEstimatedSpendAcknowledgeString(
this.state.throughput,
userContext.portalEnv,
numberOfRegions,
multimasterEnabled,
this.state.isAutoscaleSelected
);
}
private onAutoscaleRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.isAutoscaleSelected) {
this.setState({ isAutoscaleSelected: true, throughput: AutoPilotUtils.minAutoPilotThroughput });
this.props.setIsAutoscale(true);
}
}
private onManualRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && this.state.isAutoscaleSelected) {
this.setState({
isAutoscaleSelected: false,
throughput: SharedConstants.CollectionCreation.DefaultCollectionRUs400,
});
this.props.setIsAutoscale(false);
this.props.setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
}
}
}
interface CostEstimateTextProps {
requestUnits: number;
isAutoscale: boolean;
}
const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props: CostEstimateTextProps) => {
const { requestUnits, isAutoscale } = props;
const databaseAccount = userContext.databaseAccount;
if (!databaseAccount || !databaseAccount.properties) {
return <></>;
}
const serverId: string = userContext.portalEnv;
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
const hourlyPrice: number = PricingUtils.computeRUUsagePriceHourly({
serverId,
requestUnits,
numberOfRegions,
multimasterEnabled,
isAutoscale,
});
const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * SharedConstants.hoursInAMonth;
const currency: string = PricingUtils.getPriceCurrency(serverId);
const currencySign: string = PricingUtils.getCurrencySign(serverId);
const multiplier = PricingUtils.getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale
? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * multiplier
: PricingUtils.getPricePerRu(serverId) * multiplier;
if (isAutoscale) {
return (
<Text variant="small">
Estimated monthly cost ({currency}):{" "}
<b>
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
);
}
return (
<Text variant="small">
Cost ({currency}):{" "}
<b>
{currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "}
{currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
<br />
<em>{PricingUtils.estimatedCostDisclaimer}</em>
</Text>
);
};

View File

@@ -2,17 +2,17 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument"); jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q";
import { createDocument } from "../../Common/dataAccess/createDocument";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext"; import Q from "q";
import Explorer from "../Explorer";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { createDocument } from "../../Common/dataAccess/createDocument";
import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext";
describe("ContainerSampleGenerator", () => { describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => { const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer; const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]); explorerStub.nonSystemDatabases = ko.computed(() => [database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false); explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false);

View File

@@ -1,9 +1,9 @@
import * as ko from "knockout";
import * as sinon from "sinon";
import { Collection, Database } from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { DataSamplesUtil } from "./DataSamplesUtil"; import { DataSamplesUtil } from "./DataSamplesUtil";
import * as sinon from "sinon";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as ko from "knockout";
import Explorer from "../Explorer";
import { Database, Collection } from "../../Contracts/ViewModels";
describe("DataSampleUtils", () => { describe("DataSampleUtils", () => {
const sampleCollectionId = "sampleCollectionId"; const sampleCollectionId = "sampleCollectionId";
@@ -16,7 +16,7 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]), collections: ko.observableArray<Collection>([collection]),
} as Database; } as Database;
const explorer = {} as Explorer; const explorer = {} as Explorer;
explorer.databases = ko.observableArray<Database>([database]); explorer.nonSystemDatabases = ko.computed(() => [database]);
explorer.showOkModalDialog = () => {}; explorer.showOkModalDialog = () => {};
const dataSamplesUtil = new DataSamplesUtil(explorer); const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,8 +1,8 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import Explorer from "../Explorer";
export class DataSamplesUtil { export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container"; private static readonly DialogTitle = "Create Sample Container";
@@ -17,7 +17,7 @@ export class DataSamplesUtil {
const databaseName = generator.getDatabaseId(); const databaseName = generator.getDatabaseId();
const containerName = generator.getCollectionId(); const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, this.container.databases())) { if (this.hasContainer(databaseName, containerName, this.container.nonSystemDatabases())) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);

View File

@@ -1,43 +0,0 @@
jest.mock("./../Common/dataAccess/deleteDatabase");
jest.mock("./../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase";
import * as ViewModels from "./../Contracts/ViewModels";
import Explorer from "./Explorer";
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: Explorer;
beforeAll(() => {
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
});
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if only 1 database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
const database = {} as ViewModels.Database;
const database2 = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
const database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
const database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});

View File

@@ -21,7 +21,7 @@ import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem, IPinnedRepo } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { RouteHandler } from "../RouteHandlers/RouteHandler";
@@ -44,19 +44,18 @@ import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import { FileSystemUtil } from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane"; import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel"; import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane"; import { LoadQueryPane } from "./Panes/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane"; import NewVertexPane from "./Panes/NewVertexPane";
@@ -70,6 +69,7 @@ import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { UploadFilePane } from "./Panes/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase"; import TabsBase from "./Tabs/TabsBase";
@@ -77,7 +77,6 @@ import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger"; import Trigger from "./Tree/Trigger";
@@ -95,6 +94,10 @@ export interface ExplorerParams {
closeSidePanel: () => void; closeSidePanel: () => void;
closeDialog: () => void; closeDialog: () => void;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
onRefreshNotebookList: () => void;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
getMyNotebooksContentRoot: () => NotebookContentItem;
} }
export default class Explorer { export default class Explorer {
@@ -163,6 +166,8 @@ export default class Explorer {
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>; public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>; public features: ko.Observable<any>;
public serverId: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public queriesClient: QueriesClient; public queriesClient: QueriesClient;
public tableDataClient: TableDataClient; public tableDataClient: TableDataClient;
public splitter: Splitter; public splitter: Splitter;
@@ -179,19 +184,25 @@ export default class Explorer {
// Resource Tree // Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>; public databases: ko.ObservableArray<ViewModels.Database>;
public nonSystemDatabases: ko.Computed<ViewModels.Database[]>;
public selectedDatabaseId: ko.Computed<string>; public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>; public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>; public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
private resourceTree: ResourceTreeAdapter; /**
* @deprecated
* Use a local loading state and spinner instead. Using a global isRefreshing state causes problems.
* */
public isRefreshingExplorer: ko.Observable<boolean>;
// Resource Token // Resource Token
public resourceTokenDatabaseId: ko.Observable<string>; public resourceTokenDatabaseId: ko.Observable<string>;
public resourceTokenCollectionId: ko.Observable<string>; public resourceTokenCollectionId: ko.Observable<string>;
public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>; public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>;
public resourceTokenPartitionKey: ko.Observable<string>; public resourceTokenPartitionKey: ko.Observable<string>;
public isAuthWithResourceToken: ko.Observable<boolean>;
public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>; public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; private resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
// Tabs // Tabs
public isTabsContentExpanded: ko.Observable<boolean>; public isTabsContentExpanded: ko.Observable<boolean>;
@@ -203,6 +214,7 @@ export default class Explorer {
public addDatabasePane: AddDatabasePane; public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane; public addCollectionPane: AddCollectionPane;
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane; public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
public deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane;
public graphStylingPane: GraphStylingPane; public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane; public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane; public editTableEntityPane: EditTableEntityPane;
@@ -210,9 +222,14 @@ export default class Explorer {
public querySelectPane: QuerySelectPane; public querySelectPane: QuerySelectPane;
public newVertexPane: NewVertexPane; public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public settingsPane: SettingsPane;
public executeSprocParamsPane: ExecuteSprocParamsPane;
public uploadItemsPane: UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: LoadQueryPane; public loadQueryPane: LoadQueryPane;
public saveQueryPane: ContextualPaneBase; public saveQueryPane: ContextualPaneBase;
public browseQueriesPane: BrowseQueriesPane; public browseQueriesPane: BrowseQueriesPane;
public uploadFilePane: UploadFilePane;
public stringInputPane: StringInputPane; public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane; public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
@@ -249,6 +266,7 @@ export default class Explorer {
public closeDialog: ExplorerParams["closeDialog"]; public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = []; private _panes: ContextualPaneBase[] = [];
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = (database) => false;
private _isInitializingNotebooks: boolean; private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>; private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager; private _arcadiaManager: ArcadiaResourceManager;
@@ -262,7 +280,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(params?: ExplorerParams) { constructor(public params?: ExplorerParams) {
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded; this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
this.setNotificationConsoleData = params?.setNotificationConsoleData; this.setNotificationConsoleData = params?.setNotificationConsoleData;
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted; this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
@@ -285,6 +303,22 @@ export default class Explorer {
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>(); this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType); this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
let firstInitialization = true;
this.isRefreshingExplorer = ko.observable<boolean>(true);
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
if (!isRefreshing && firstInitialization) {
// set focus on first element
firstInitialization = false;
try {
document.getElementById("createNewContainerCommandButton").parentElement.parentElement.focus();
} catch (e) {
Logger.logWarning(
"getElementById('createNewContainerCommandButton') failed to find element",
"Explorer/this.isRefreshingExplorer.subscribe"
);
}
}
});
this.isAccountReady = ko.observable<boolean>(false); this.isAccountReady = ko.observable<boolean>(false);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
@@ -305,9 +339,7 @@ export default class Explorer {
this.isSynapseLinkUpdating = ko.observable<boolean>(false); this.isSynapseLinkUpdating = ko.observable<boolean>(false);
this.isAccountReady.subscribe(async (isAccountReady: boolean) => { this.isAccountReady.subscribe(async (isAccountReady: boolean) => {
if (isAccountReady) { if (isAccountReady) {
userContext.authType === AuthType.ResourceToken this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(true);
RouteHandler.getInstance().initHandler(); RouteHandler.getInstance().initHandler();
this.notebookWorkspaceManager = new NotebookWorkspaceManager(); this.notebookWorkspaceManager = new NotebookWorkspaceManager();
this.arcadiaWorkspaces = ko.observableArray(); this.arcadiaWorkspaces = ko.observableArray();
@@ -318,9 +350,9 @@ export default class Explorer {
Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then( Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then(
async () => { async () => {
this.isNotebookEnabled( this.isNotebookEnabled(
userContext.authType !== AuthType.ResourceToken && !this.isAuthWithResourceToken() &&
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) || ((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
userContext.features.enableNotebooks) this.isFeatureEnabled(Constants.Features.enableNotebooks))
); );
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
@@ -342,7 +374,7 @@ export default class Explorer {
this.isSparkEnabledForAccount() && this.isSparkEnabledForAccount() &&
this.arcadiaWorkspaces() && this.arcadiaWorkspaces() &&
this.arcadiaWorkspaces().length > 0) || this.arcadiaWorkspaces().length > 0) ||
userContext.features.enableSpark this.isFeatureEnabled(Constants.Features.enableSpark)
); );
if (this.isSparkEnabled()) { if (this.isSparkEnabled()) {
appInsights.trackEvent( appInsights.trackEvent(
@@ -366,20 +398,26 @@ export default class Explorer {
}); });
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>(); this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.features = ko.observable();
this.serverId = ko.observable<string>();
this.queriesClient = new QueriesClient(this); this.queriesClient = new QueriesClient(this);
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
this.resourceTokenDatabaseId = ko.observable<string>(); this.resourceTokenDatabaseId = ko.observable<string>();
this.resourceTokenCollectionId = ko.observable<string>(); this.resourceTokenCollectionId = ko.observable<string>();
this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>(); this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>();
this.resourceTokenPartitionKey = ko.observable<string>(); this.resourceTokenPartitionKey = ko.observable<string>();
this.isAuthWithResourceToken = ko.observable<boolean>(false);
this.isGitHubPaneEnabled = ko.observable<boolean>(false); this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isMongoIndexingEnabled = ko.observable<boolean>(false); this.isMongoIndexingEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false); this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false); this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
this.canExceedMaximumValue = ko.computed<boolean>(() => userContext.features.canExceedMaximumValue); this.canExceedMaximumValue = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
);
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema); this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false); this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
@@ -459,7 +497,7 @@ export default class Explorer {
}); });
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) { if (this.isFeatureEnabled(Constants.Features.enableFixedCollectionWithSharedThroughput)) {
return true; return true;
} }
@@ -518,7 +556,20 @@ export default class Explorer {
() => () =>
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
); );
this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2); this.isRightPanelV2Enabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
);
this.defaultExperience.subscribe((defaultExperience: string) => {
if (
defaultExperience &&
defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase()
) {
this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system";
};
}
});
this.selectedDatabaseId = ko.computed<string>(() => { this.selectedDatabaseId = ko.computed<string>(() => {
const selectedNode = this.selectedNode(); const selectedNode = this.selectedNode();
if (!selectedNode) { if (!selectedNode) {
@@ -540,6 +591,10 @@ export default class Explorer {
} }
}); });
this.nonSystemDatabases = ko.computed(() => {
return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database));
});
this.addDatabasePane = new AddDatabasePane({ this.addDatabasePane = new AddDatabasePane({
id: "adddatabasepane", id: "adddatabasepane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -562,6 +617,13 @@ export default class Explorer {
container: this, container: this,
}); });
this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.graphStylingPane = new GraphStylingPane({ this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane", id: "graphstylingpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -611,6 +673,29 @@ export default class Explorer {
container: this, container: this,
}); });
this.settingsPane = new SettingsPane({
id: "settingspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.executeSprocParamsPane = new ExecuteSprocParamsPane({
id: "executesprocparamspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadItemsPane = new UploadItemsPane({
id: "uploaditemspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.loadQueryPane = new LoadQueryPane({ this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane", id: "loadquerypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -632,6 +717,13 @@ export default class Explorer {
container: this, container: this,
}); });
this.uploadFilePane = new UploadFilePane({
id: "uploadfilepane",
visible: ko.observable<boolean>(false),
container: this,
});
this.stringInputPane = new StringInputPane({ this.stringInputPane = new StringInputPane({
id: "stringinputpane", id: "stringinputpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -652,6 +744,7 @@ export default class Explorer {
this.addDatabasePane, this.addDatabasePane,
this.addCollectionPane, this.addCollectionPane,
this.deleteCollectionConfirmationPane, this.deleteCollectionConfirmationPane,
this.deleteDatabaseConfirmationPane,
this.graphStylingPane, this.graphStylingPane,
this.addTableEntityPane, this.addTableEntityPane,
this.editTableEntityPane, this.editTableEntityPane,
@@ -659,9 +752,13 @@ export default class Explorer {
this.querySelectPane, this.querySelectPane,
this.newVertexPane, this.newVertexPane,
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.settingsPane,
this.executeSprocParamsPane,
this.uploadItemsPane,
this.loadQueryPane, this.loadQueryPane,
this.saveQueryPane, this.saveQueryPane,
this.browseQueriesPane, this.browseQueriesPane,
this.uploadFilePane,
this.stringInputPane, this.stringInputPane,
this.setupNotebooksPane, this.setupNotebooksPane,
]; ];
@@ -757,6 +854,8 @@ export default class Explorer {
this.editTableEntityPane.title("Edit Table Row"); this.editTableEntityPane.title("Edit Table Row");
this.deleteCollectionConfirmationPane.title("Delete Table"); this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
} }
@@ -781,7 +880,6 @@ export default class Explorer {
this.notebookManager.initialize({ this.notebookManager.initialize({
container: this, container: this,
notebookBasePath: this.notebookBasePath, notebookBasePath: this.notebookBasePath,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(), refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(), refreshNotebookList: () => this.refreshNotebookList(),
}); });
@@ -796,7 +894,6 @@ export default class Explorer {
this.isSparkEnabled = ko.observable(false); this.isSparkEnabled = ko.observable(false);
this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons());
this.resourceTree = new ResourceTreeAdapter(this);
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({ this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
notebookServerEndpoint: undefined, notebookServerEndpoint: undefined,
@@ -810,29 +907,42 @@ export default class Explorer {
}); });
// Override notebook server parameters from URL parameters // Override notebook server parameters from URL parameters
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) { const featureSubcription = this.features.subscribe((features) => {
this.notebookServerInfo({ const serverInfo = this.notebookServerInfo();
notebookServerEndpoint: userContext.features.notebookServerUrl, if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) {
authToken: userContext.features.notebookServerToken, serverInfo.notebookServerEndpoint = features[Constants.Features.notebookServerUrl];
}); }
}
if (userContext.features.notebookBasePath) { if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) {
this.notebookBasePath(userContext.features.notebookBasePath); serverInfo.authToken = features[Constants.Features.notebookServerToken];
} }
this.notebookServerInfo(serverInfo);
this.notebookServerInfo.valueHasMutated();
if (userContext.features.livyEndpoint) { if (this.isFeatureEnabled(Constants.Features.notebookBasePath)) {
this.sparkClusterConnectionInfo({ this.notebookBasePath(features[Constants.Features.notebookBasePath]);
userName: undefined, }
password: undefined,
endpoints: [ if (this.isFeatureEnabled(Constants.Features.livyEndpoint)) {
{ this.sparkClusterConnectionInfo({
endpoint: userContext.features.livyEndpoint, userName: undefined,
kind: DataModels.SparkClusterEndpointKind.Livy, password: undefined,
}, endpoints: [
], {
}); endpoint: features[Constants.Features.livyEndpoint],
} kind: DataModels.SparkClusterEndpointKind.Livy,
},
],
});
this.sparkClusterConnectionInfo.valueHasMutated();
}
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
updateUserContext({ useSDKOperations: true });
}
featureSubcription.dispose();
});
} }
public openEnableSynapseLinkDialog(): void { public openEnableSynapseLinkDialog(): void {
@@ -916,6 +1026,20 @@ export default class Explorer {
return this.selectedNode() == null; return this.selectedNode() == null;
} }
public isFeatureEnabled(feature: string): boolean {
const features = this.features();
if (!features) {
return false;
}
if (feature in features && features[feature]) {
return true;
}
return false;
}
public logConsoleData(consoleData: ConsoleData): void { public logConsoleData(consoleData: ConsoleData): void {
this.setNotificationConsoleData(consoleData); this.setNotificationConsoleData(consoleData);
} }
@@ -962,6 +1086,7 @@ export default class Explorer {
} }
public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> { public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> {
this.isRefreshingExplorer(true);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
@@ -991,19 +1116,22 @@ export default class Explorer {
this.deleteDatabasesFromList(deltaDatabases.toDelete); this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode); this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers..."); this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
() => { .then(
this._setLoadingStatusText("Successfully fetched containers."); () => {
deferred.resolve(); this._setLoadingStatusText("Successfully fetched containers.");
}, deferred.resolve();
(reason) => { },
this._setLoadingStatusText("Failed to fetch containers."); (reason) => {
deferred.reject(reason); this._setLoadingStatusText("Failed to fetch containers.");
} deferred.reject(reason);
); }
)
.finally(() => this.isRefreshingExplorer(false));
}, },
(error) => { (error) => {
this._setLoadingStatusText("Failed to fetch databases."); this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false);
deferred.reject(error); deferred.reject(error);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
@@ -1063,9 +1191,8 @@ export default class Explorer {
description: "Refresh button clicked", description: "Refresh button clicked",
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
userContext.authType === AuthType.ResourceToken this.isRefreshingExplorer(true);
? this.refreshDatabaseForResourceToken() this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases();
: this.refreshAllDatabases();
this.refreshNotebookList(); this.refreshNotebookList();
}; };
@@ -1158,12 +1285,12 @@ export default class Explorer {
throw error; throw error;
} finally { } finally {
// Overwrite with feature flags // Overwrite with feature flags
if (userContext.features.notebookServerUrl) { if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) {
connectionInfo.notebookServerEndpoint = userContext.features.notebookServerUrl; connectionInfo.notebookServerEndpoint = this.features()[Constants.Features.notebookServerUrl];
} }
if (userContext.features.notebookServerToken) { if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) {
connectionInfo.authToken = userContext.features.notebookServerToken; connectionInfo.authToken = this.features()[Constants.Features.notebookServerToken];
} }
this.notebookServerInfo(connectionInfo); this.notebookServerInfo(connectionInfo);
@@ -1279,12 +1406,7 @@ export default class Explorer {
} }
public isLastNonEmptyDatabase(): boolean { public isLastNonEmptyDatabase(): boolean {
if ( if (this.isLastDatabase() && this.databases()[0].collections && this.databases()[0].collections().length > 0) {
this.isLastDatabase() &&
this.databases()[0] &&
this.databases()[0].collections &&
this.databases()[0].collections().length > 0
) {
return true; return true;
} }
return false; return false;
@@ -1318,12 +1440,16 @@ export default class Explorer {
if (inputs.defaultCollectionThroughput) { if (inputs.defaultCollectionThroughput) {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput; this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
} }
this.features(inputs.features);
this.serverId(inputs.serverId ?? Constants.ServerIds.productionPortal);
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType ?? SharedConstants.CollectionCreation.DefaultSubscriptionType); this.subscriptionType(inputs.subscriptionType ?? SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.hasWriteAccess(inputs.hasWriteAccess ?? true); this.hasWriteAccess(inputs.hasWriteAccess ?? true);
if (inputs.addCollectionDefaultFlight) { if (inputs.addCollectionDefaultFlight) {
this.flight(inputs.addCollectionDefaultFlight); this.flight(inputs.addCollectionDefaultFlight);
} }
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription ?? false);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken ?? false);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
@@ -1404,9 +1530,9 @@ export default class Explorer {
public isRunningOnNationalCloud(): boolean { public isRunningOnNationalCloud(): boolean {
return ( return (
userContext.portalEnv === "blackforest" || this.serverId() === Constants.ServerIds.blackforest ||
userContext.portalEnv === "fairfax" || this.serverId() === Constants.ServerIds.fairfax ||
userContext.portalEnv === "mooncake" this.serverId() === Constants.ServerIds.mooncake
); );
} }
@@ -1581,7 +1707,7 @@ export default class Explorer {
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
promise promise
.then(() => this.resourceTree.triggerRender()) .then(() => this.params.onRefreshNotebookList())
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason));
return promise; return promise;
} }
@@ -1589,7 +1715,7 @@ export default class Explorer {
public async importAndOpen(path: string): Promise<boolean> { public async importAndOpen(path: string): Promise<boolean> {
const name = NotebookUtil.getName(path); const name = NotebookUtil.getName(path);
const item = NotebookUtil.createNotebookContentItem(name, path, "file"); const item = NotebookUtil.createNotebookContentItem(name, path, "file");
const parent = this.resourceTree.myNotebooksContentRoot; const parent = this.params.getMyNotebooksContentRoot();
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
const existingItem = _.find(parent.children, (node) => node.name === name); const existingItem = _.find(parent.children, (node) => node.name === name);
@@ -1606,7 +1732,8 @@ export default class Explorer {
} }
public async importAndOpenContent(name: string, content: string): Promise<boolean> { public async importAndOpenContent(name: string, content: string): Promise<boolean> {
const parent = this.resourceTree.myNotebooksContentRoot; // const parent = this.params.getMyNotebooksContentRoot();
const parent = this.params.getMyNotebooksContentRoot();
if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
@@ -1791,7 +1918,6 @@ export default class Explorer {
return newNotebookFile; return newNotebookFile;
}); });
result.then(() => this.resourceTree.triggerRender());
return result; return result;
} }
@@ -1812,7 +1938,6 @@ export default class Explorer {
defaultInput: "", defaultInput: "",
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input), onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input),
}); });
result.then(() => this.resourceTree.triggerRender());
return result; return result;
} }
@@ -1968,12 +2093,14 @@ export default class Explorer {
return false; return false;
} }
}; };
private refreshNotebookList = async (): Promise<void> => { private refreshNotebookList = async (): Promise<void> => {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
return; return;
} }
await this.resourceTree.initialize(); this.params?.onRefreshNotebookList();
this.notebookManager?.refreshPinnedRepos(); this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) { if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
@@ -2036,7 +2163,7 @@ export default class Explorer {
throw new Error(error); throw new Error(error);
} }
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.params.getMyNotebooksContentRoot();
const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( const notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
@@ -2060,7 +2187,7 @@ export default class Explorer {
); );
return this.openNotebook(newFile); return this.openNotebook(newFile);
}) })
.then(() => this.resourceTree.triggerRender()) .then(() => this.params.onRefreshNotebookList())
.catch((error: any) => { .catch((error: any) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage);
@@ -2077,7 +2204,39 @@ export default class Explorer {
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId));
} }
public refreshContentItem(item: NotebookContentItem): Promise<void> { public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void {
parent = parent || this.params.getMyNotebooksContentRoot();
this.uploadFilePane.openWithOptions({
paneTitle: "Upload file to notebook server",
selectFileInputLabel: "Select file to upload",
errorMessage: "Could not upload file",
inProgressMessage: "Uploading file to notebook server",
successMessage: "Successfully uploaded file to notebook server",
onSubmit: async (file: File): Promise<NotebookContentItem> => {
const readFileAsText = (inputFile: File): Promise<string> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => {
reader.abort();
reject(`Problem parsing file: ${inputFile}`);
};
reader.onload = () => {
resolve(reader.result as string);
};
reader.readAsText(inputFile);
});
};
const fileContent = await readFileAsText(file);
return this.uploadFile(file.name, fileContent, parent);
},
extensions: undefined,
submitButtonLabel: "Upload",
});
}
public refreshContentItem(item: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled"; const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem"); handleError(error, "Explorer/refreshContentItem");
@@ -2239,12 +2398,10 @@ export default class Explorer {
public onNewCollectionClicked(): void { public onNewCollectionClicked(): void {
if (this.isPreferredApiCassandra()) { if (this.isPreferredApiCassandra()) {
this.cassandraAddCollectionPane.open(); this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableReactPane) {
this.openAddCollectionPanel();
} else { } else {
this.addCollectionPane.open(this.selectedDatabaseId()); this.addCollectionPane.open(this.selectedDatabaseId());
document.getElementById("linkAddCollection").focus();
} }
document.getElementById("linkAddCollection").focus();
} }
private refreshCommandBarButtons(): void { private refreshCommandBarButtons(): void {
@@ -2373,7 +2530,7 @@ export default class Explorer {
} }
public openDeleteCollectionConfirmationPane(): void { public openDeleteCollectionConfirmationPane(): void {
userContext.features.enableKOPanel this.isFeatureEnabled(Constants.Features.enableKOPanel)
? this.deleteCollectionConfirmationPane.open() ? this.deleteCollectionConfirmationPane.open()
: this.openSidePanel( : this.openSidePanel(
"Delete Collection", "Delete Collection",
@@ -2384,54 +2541,4 @@ export default class Explorer {
/> />
); );
} }
public openDeleteDatabaseConfirmationPane(): void {
this.openSidePanel(
"Delete Database",
<DeleteDatabaseConfirmationPanel
explorer={this}
openNotificationConsole={this.expandConsole}
closePanel={this.closeSidePanel}
selectedDatabase={this.findSelectedDatabase()}
/>
);
}
public openUploadItemsPanePane(): void {
this.openSidePanel("Upload", <UploadItemsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openSettingPane(): void {
this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openExecuteSprocParamsPanel(): void {
this.openSidePanel(
"Input parameters",
<ExecuteSprocParamsPanel explorer={this} closePanel={() => this.closeSidePanel()} />
);
}
public async openAddCollectionPanel(): Promise<void> {
await this.loadDatabaseOffers();
this.openSidePanel(
"New Collection",
<AddCollectionPanel
explorer={this}
closePanel={() => this.closeSidePanel()}
openNotificationConsole={() => this.expandConsole()}
/>
);
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.openSidePanel(
"Upload File",
<UploadFilePane
explorer={this}
closePanel={this.closeSidePanel}
uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)}
/>
);
}
} }

View File

@@ -4,8 +4,11 @@
* - inspired from gremlin-javascript for nodejs: https://github.com/jbmusso/gremlin-javascript * - inspired from gremlin-javascript for nodejs: https://github.com/jbmusso/gremlin-javascript
* - tested on cosmosdb gremlin server * - tested on cosmosdb gremlin server
* - only supports sessionless gremlin requests * - only supports sessionless gremlin requests
* - Relies on text-encoding polyfill (github.com/inexorabletash/text-encoding) for TextEncoder/TextDecoder on IE, Edge.
*/ */
import { TextEncoder, TextDecoder } from "text-encoding";
export interface GremlinSimpleClientParameters { export interface GremlinSimpleClientParameters {
endpoint: string; // The websocket endpoint endpoint: string; // The websocket endpoint
user: string; user: string;

View File

@@ -1,10 +1,8 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { AuthType } from "../../../AuthType";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import NotebookManager from "../../Notebook/NotebookManager";
import Explorer from "../../Explorer";
describe("CommandBarComponentButtonFactory tests", () => { describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer; let mockExplorer: Explorer;
@@ -15,6 +13,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
@@ -54,6 +53,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
@@ -118,6 +118,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
@@ -198,6 +199,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -279,6 +281,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
@@ -337,13 +340,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(true);
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true); mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
updateUserContext({
authType: AuthType.ResourceToken,
});
}); });
it("should only show New SQL Query and Open Query buttons", () => { it("should only show New SQL Query and Open Query buttons", () => {

View File

@@ -1,38 +1,37 @@
import * as React from "react"; import * as ViewModels from "../../../Contracts/ViewModels";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg"; import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg"; import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import * as Constants from "../../../Common/Constants";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg"; import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import GitHubIcon from "../../../../images/github.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg"; import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg"; import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg"; import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg"; import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import GitHubIcon from "../../../../images/github.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg"; import SynapseIcon from "../../../../images/synapse-link.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { Areas } from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as React from "react";
import { OpenFullScreen } from "../../OpenFullScreen"; import { OpenFullScreen } from "../../OpenFullScreen";
let counter = 0; let counter = 0;
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) { if (container.isAuthWithResourceToken()) {
return createStaticCommandBarButtonsForResourceToken(container); return createStaticCommandBarButtonsForResourceToken(container);
} }
@@ -164,7 +163,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
const settingsPaneButton: CommandButtonComponentProps = { const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.openSettingPane(), onCommandClick: () => container.settingsPane.open(),
commandButtonLabel: undefined, commandButtonLabel: undefined,
ariaLabel: label, ariaLabel: label,
tooltipText: label, tooltipText: label,
@@ -407,7 +406,7 @@ function createuploadNotebookButton(container: Explorer): CommandButtonComponent
return { return {
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.openUploadFilePanel(), onCommandClick: () => container.onUploadToNotebookServerClicked(),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: false, disabled: false,

View File

@@ -1,33 +1,37 @@
/** // Utilities for file system
* file list returns path starting with ./blah
* rename returns simply blah.
* Both are the same. This method only handles these two cases and no other complicated paths that may contain ..
* ./ inside the path.
* TODO: this should go away when not using jupyter for file operations and use normalized paths.
* @param path1
* @param path2
*/
export function isPathEqual(path1: string, path2: string): boolean {
const normalize = (path: string): string => {
const dotSlash = "./";
if (path.indexOf(dotSlash) === 0) {
path = path.substring(dotSlash.length);
}
return path;
};
return normalize(path1) === normalize(path2); export class FileSystemUtil {
} /**
* file list returns path starting with ./blah
* rename returns simply blah.
* Both are the same. This method only handles these two cases and no other complicated paths that may contain ..
* ./ inside the path.
* TODO: this should go away when not using jupyter for file operations and use normalized paths.
* @param path1
* @param path2
*/
public static isPathEqual(path1: string, path2: string): boolean {
const normalize = (path: string): string => {
const dotSlash = "./";
if (path.indexOf(dotSlash) === 0) {
path = path.substring(dotSlash.length);
}
return path;
};
/** return normalize(path1) === normalize(path2);
* Remove extension }
* @param path
* @param extension Without the ".". e.g. "ipynb" (and not ".ipynb") /**
*/ * Remove extension
export function stripExtension(path: string, extension: string): string { * @param path
const splitted = path.split("."); * @param extension Without the ".". e.g. "ipynb" (and not ".ipynb")
if (splitted[splitted.length - 1] === extension) { */
splitted.pop(); public static stripExtension(path: string, extension: string): string {
const splitted = path.split(".");
if (splitted[splitted.length - 1] === extension) {
splitted.pop();
}
return splitted.join(".");
} }
return splitted.join(".");
} }

View File

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

View File

@@ -1,4 +1,4 @@
import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer, from } from "rxjs"; import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { webSocket } from "rxjs/webSocket"; import { webSocket } from "rxjs/webSocket";
import { StateObservable } from "redux-observable"; import { StateObservable } from "redux-observable";
import { ofType } from "redux-observable"; import { ofType } from "redux-observable";
@@ -44,7 +44,7 @@ import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils"; import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file"; import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
import * as FileSystemUtil from "../FileSystemUtil"; import { FileSystemUtil } from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { Areas } from "../../../Common/Constants"; import { Areas } from "../../../Common/Constants";
@@ -944,39 +944,6 @@ const traceNotebookKernelEpic = (
); );
}; };
const resetCellStatusOnExecuteCanceledEpic = (
action$: Observable<actions.ExecuteCanceled>,
state$: StateObservable<AppState>
): Observable<actions.UpdateCellStatus> => {
return action$.pipe(
ofType(actions.EXECUTE_CANCELED),
mergeMap((action) => {
const contentRef = action.payload.contentRef;
const model = state$.value.core.entities.contents.byRef.get(contentRef).model;
let busyCellIds: string[] = [];
if (model.type === "notebook") {
const cellMap = model.transient.get("cellMap");
if (cellMap) {
for (const entry of cellMap.toArray()) {
const cellId = entry[0];
const status = model.transient.getIn(["cellMap", cellId, "status"]);
if (status === "busy") {
busyCellIds.push(cellId);
}
}
}
}
return from(busyCellIds).pipe(
map((busyCellId) => {
return actions.updateCellStatus({ id: busyCellId, contentRef, status: undefined });
})
);
})
);
};
export const allEpics = [ export const allEpics = [
addInitialCodeCellEpic, addInitialCodeCellEpic,
focusInitialCodeCellEpic, focusInitialCodeCellEpic,
@@ -993,5 +960,4 @@ export const allEpics = [
traceNotebookTelemetryEpic, traceNotebookTelemetryEpic,
traceNotebookInfoEpic, traceNotebookInfoEpic,
traceNotebookKernelEpic, traceNotebookKernelEpic,
resetCellStatusOnExecuteCanceledEpic,
]; ];

View File

@@ -1,12 +1,13 @@
import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import * as StringUtils from "../../Utils/StringUtils";
import { FileSystemUtil } from "./FileSystemUtil";
import { NotebookUtil } from "./NotebookUtil"; import { NotebookUtil } from "./NotebookUtil";
import { ServerConfig, IContent, IContentProvider, FileType, IEmptyContent } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import { stringifyNotebook } from "@nteract/commutable";
export class NotebookContentClient { export class NotebookContentClient {
constructor( constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>, private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
@@ -17,11 +18,13 @@ export class NotebookContentClient {
/** /**
* This updates the item and points all the children's parent to this item * This updates the item and points all the children's parent to this item
* @param item * @param item
* @return updated item
*/ */
public updateItemChildren(item: NotebookContentItem): Promise<void> { public updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
return this.fetchNotebookFiles(item.path).then((subItems) => { return this.fetchNotebookFiles(item.path).then((subItems) => {
item.children = subItems; item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item)); subItems.forEach((subItem) => (subItem.parent = item));
return item;
}); });
} }

View File

@@ -18,7 +18,6 @@ import { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { MemoryUsageInfo } from "../../Contracts/DataModels"; import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable"; import { ImmutableNotebook } from "@nteract/commutable";
@@ -30,7 +29,6 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
notebookBasePath: ko.Observable<string>; notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void; refreshCommandBarButtons: () => void;
refreshNotebookList: () => void; refreshNotebookList: () => void;
} }
@@ -107,8 +105,8 @@ export default class NotebookManager {
}); });
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos); // TODO Move this out of NotebookManager?
this.params.resourceTree.triggerRender(); this.params.container.params.initializeGitHubRepos(pinnedRepos);
}); });
this.refreshPinnedRepos(); this.refreshPinnedRepos();
} }

View File

@@ -1,63 +0,0 @@
import * as React from "react";
import styled from "styled-components";
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";
}
const StyledIFrame = styled.iframe`
width: 100%;
border-style: unset;
`;
export class IFrameHTML extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "text/html"
};
frame?: HTMLIFrameElement;
appendChildDOM(): void {
if (!this.frame) {
return;
}
this.frame.contentDocument.open();
this.frame.contentDocument.write(this.props.data);
this.frame.contentDocument.close();
}
componentDidMount(): void {
this.appendChildDOM();
}
componentDidUpdate(): void {
this.appendChildDOM();
}
render() {
return (
<StyledIFrame
ref={frame => this.frame = frame}
allow="accelerometer; autoplay; camera; gyroscope; magnetometer; microphone; xr-spatial-tracking"
sandbox="allow-downloads allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-popups-to-escape-sandbox"
onLoad={() => this.onFrameLoaded()} />
);
}
onFrameLoaded() {
this.frame.height = (this.frame.contentDocument.body.scrollHeight + 4) + "px";
this.frame.contentDocument.body.style.margin = "0px";
}
}
export default IFrameHTML;

View File

@@ -1,28 +0,0 @@
import React from "react";
import IFrameHTML from "./IFrameHTML";
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 IFrameJavaScript extends React.PureComponent<Props> {
static defaultProps = {
data: "",
mediaType: "application/javascript"
};
render() {
return (
<IFrameHTML data={`<script>${this.props.data}</script>`} />
);
}
}
export default IFrameJavaScript;

View File

@@ -1,7 +1,7 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { ActionContracts } from "../Contracts/ExplorerContracts";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
export function handleOpenAction( export function handleOpenAction(
@@ -145,7 +145,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) { ) {
explorer.closeAllPanes(); explorer.closeAllPanes();
explorer.openSettingPane(); explorer.settingsPane.open();
} }
} }

View File

@@ -1,22 +1,22 @@
import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import { createCollection } from "../../Common/dataAccess/createCollection";
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 AddCollectionUtility from "../../Shared/AddCollectionUtility"; import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
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 AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as PricingUtils from "../../Utils/PricingUtils";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent"; import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { configContext, Platform } from "../../ConfigContext";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { userContext } from "../../UserContext";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>; isPreferredApiTable: ko.Computed<boolean>;
@@ -49,7 +49,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
public throughputDatabase: ViewModels.Editable<number>; public throughputDatabase: ViewModels.Editable<number>;
public isPreferredApiTable: ko.Computed<boolean>; public isPreferredApiTable: ko.Computed<boolean>;
public partitionKeyPlaceholder: ko.Computed<string>; public partitionKeyPlaceholder: ko.Computed<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>; public isTryCosmosDBSubscription: ko.Computed<boolean>;
public maxThroughputRU: ko.Observable<number>; public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>; public minThroughputRU: ko.Observable<number>;
public throughputRangeText: ko.Computed<string>; public throughputRangeText: ko.Computed<string>;
@@ -186,6 +186,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId: string = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -199,28 +200,23 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (!this.isSharedAutoPilotSelected()) { if (!this.isSharedAutoPilotSelected()) {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster multimaster
); );
@@ -244,6 +240,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId: string = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -257,28 +254,28 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.throughputMultiPartition(), this.throughputMultiPartition(),
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.throughputMultiPartition(), this.throughputMultiPartition(),
userContext.portalEnv, serverId,
regions, regions,
multimaster multimaster
); );
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.autoPilotThroughput(), this.autoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.autoPilotThroughput(), this.autoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster multimaster
); );
@@ -288,7 +285,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
return estimatedSpend; return estimatedSpend;
}); });
this.isTryCosmosDBSubscription = ko.observable<boolean>(userContext.isTryCosmosDBSubscription || false); this.isTryCosmosDBSubscription = ko.pureComputed<boolean>(() => {
return (this.container && this.container.isTryCosmosDBSubscription()) || false;
});
this.isTryCosmosDBSubscription.subscribe((isTryCosmosDB: boolean) => { this.isTryCosmosDBSubscription.subscribe((isTryCosmosDB: boolean) => {
if (!!isTryCosmosDB) { if (!!isTryCosmosDB) {
@@ -299,7 +298,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if ( if (
configContext.platform !== Platform.Emulator && configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription && !this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal configContext.platform !== Platform.Portal
) { ) {
const offerThroughput: number = this._getThroughput(); const offerThroughput: number = this._getThroughput();
@@ -490,7 +489,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.upsellMessage = ko.pureComputed<string>(() => { this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage( return PricingUtils.getUpsellMessage(
userContext.portalEnv, this.container.serverId(),
this.isFreeTierAccount(), this.isFreeTierAccount(),
this.container.isFirstResourceCreated(), this.container.isFirstResourceCreated(),
this.container.defaultExperience(), this.container.defaultExperience(),
@@ -994,7 +993,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.container.openEnableSynapseLinkDialog(); this.container.openEnableSynapseLinkDialog();
} }
public ttl90DaysEnabled: () => boolean = () => userContext.features.ttl90Days; public ttl90DaysEnabled: () => boolean = () => this.container.isFeatureEnabled(Constants.Features.ttl90Days);
public isValid(): boolean { public isValid(): boolean {
// TODO add feature flag that disables validation for customers with custom accounts // TODO add feature flag that disables validation for customers with custom accounts
@@ -1202,7 +1201,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (this.isAnalyticalStorageOn()) { if (this.isAnalyticalStorageOn()) {
// TODO: always default to 90 days once the backend hotfix is deployed // TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days return this.container.isFeatureEnabled(Constants.Features.ttl90Days)
? Constants.AnalyticalStorageTtl.Days90 ? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite; : Constants.AnalyticalStorageTtl.Infinite;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,19 @@
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 AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { userContext } from "../../UserContext";
export default class AddDatabasePane extends ContextualPaneBase { export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>; public defaultExperience: ko.Computed<string>;
@@ -122,6 +122,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
return ""; return "";
} }
const serverId = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -133,15 +134,10 @@ export default class AddDatabasePane extends ContextualPaneBase {
let estimatedSpendAcknowledge: string; let estimatedSpendAcknowledge: string;
let estimatedSpend: string; let estimatedSpend: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -149,13 +145,13 @@ export default class AddDatabasePane extends ContextualPaneBase {
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.maxAutoPilotThroughputSet(), this.maxAutoPilotThroughputSet(),
userContext.portalEnv, serverId,
regions, regions,
multimaster multimaster
); );
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.maxAutoPilotThroughputSet(), this.maxAutoPilotThroughputSet(),
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -169,7 +165,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if ( if (
configContext.platform !== Platform.Emulator && configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription && !this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal configContext.platform !== Platform.Portal
) { ) {
const offerThroughput: number = this.throughput(); const offerThroughput: number = this.throughput();
@@ -243,7 +239,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.upsellMessage = ko.pureComputed<string>(() => { this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage( return PricingUtils.getUpsellMessage(
userContext.portalEnv, this.container.serverId(),
this.isFreeTierAccount(), this.isFreeTierAccount(),
this.container.isFirstResourceCreated(), this.container.isFirstResourceCreated(),
this.container.defaultExperience(), this.container.defaultExperience(),

View File

@@ -114,7 +114,7 @@
aria-label="Keyspace id" aria-label="Keyspace id"
/> />
<datalist id="keyspacesList" data-bind="foreach: container.databases"> <datalist id="keyspacesList" data-bind="foreach: container.nonSystemDatabases">
<option data-bind="value: $data.id"></option> <option data-bind="value: $data.id"></option>
</datalist> </datalist>

View File

@@ -1,21 +1,21 @@
import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility"; import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
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 AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { userContext } from "../../UserContext";
export default class CassandraAddCollectionPane extends ContextualPaneBase { export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>; public createTableQuery: ko.Observable<string>;
@@ -127,6 +127,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -138,15 +139,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedDedicatedSpendAcknowledge: string; let estimatedDedicatedSpendAcknowledge: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -154,13 +150,13 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.selectedAutoPilotThroughput(), this.selectedAutoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster multimaster
); );
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.selectedAutoPilotThroughput(), this.selectedAutoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isAutoPilotSelected() this.isAutoPilotSelected()
@@ -176,6 +172,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const serverId = this.container.serverId();
const regions = const regions =
(account && (account &&
account.properties && account.properties &&
@@ -186,15 +183,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedSharedSpendAcknowledge: string; let estimatedSharedSpendAcknowledge: string;
if (!this.isSharedAutoPilotSelected()) { if (!this.isSharedAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster);
this.keyspaceThroughput(),
userContext.portalEnv,
regions,
multimaster
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.keyspaceThroughput(), this.keyspaceThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
@@ -202,13 +194,13 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster multimaster
); );
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
userContext.portalEnv, serverId,
regions, regions,
multimaster, multimaster,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
@@ -223,7 +215,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}); });
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform !== Platform.Emulator && !userContext.isTryCosmosDBSubscription) { if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
const offerThroughput: number = this.throughput(); const offerThroughput: number = this.throughput();
return offerThroughput <= 100000; return offerThroughput <= 100000;
} }
@@ -261,8 +253,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}); });
this.keyspaceIds(cachedKeyspaceIdsList); this.keyspaceIds(cachedKeyspaceIdsList);
}; };
this.container.databases.subscribe((newDatabases: ViewModels.Database[]) => updateKeyspaceIds(newDatabases)); this.container.nonSystemDatabases.subscribe((newDatabases: ViewModels.Database[]) =>
updateKeyspaceIds(this.container.databases()); updateKeyspaceIds(newDatabases)
);
updateKeyspaceIds(this.container.nonSystemDatabases());
} }
this.autoPilotUsageCost = ko.pureComputed<string>(() => { this.autoPilotUsageCost = ko.pureComputed<string>(() => {

View File

@@ -8,10 +8,9 @@ import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRight
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
import { IDropdownOption } from "office-ui-fabric-react"; import { IDropdownOption } from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants"; import { HttpStatusCodes, Notebook } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location { interface Location {
@@ -151,7 +150,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
name: ResourceTreeAdapter.MyNotebooksTitle, name: Notebook.MyNotebooksTitle,
path: this.container.getNotebookBasePath(), path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -159,7 +158,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
case "GitHub": case "GitHub":
parent = { parent = {
name: ResourceTreeAdapter.GitHubReposTitle, name: Notebook.GitHubReposTitle,
path: GitHubUtils.toContentUri( path: GitHubUtils.toContentUri(
this.selectedLocation.owner, this.selectedLocation.owner,
this.selectedLocation.repo, this.selectedLocation.repo,

View File

@@ -1,7 +1,6 @@
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as React from "react"; import * as React from "react";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { import {
Stack, Stack,
Label, Label,
@@ -13,6 +12,7 @@ import {
IRenderFunction, IRenderFunction,
ISelectableOption, ISelectableOption,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { Notebook } from "../../Common/Constants";
interface Location { interface Location {
type: "MyNotebooks" | "GitHub"; type: "MyNotebooks" | "GitHub";
@@ -70,8 +70,8 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
options.push({ options.push({
key: "MyNotebooks-Item", key: "MyNotebooks-Item",
text: ResourceTreeAdapter.MyNotebooksTitle, text: Notebook.MyNotebooksTitle,
title: ResourceTreeAdapter.MyNotebooksTitle, title: Notebook.MyNotebooksTitle,
data: { data: {
type: "MyNotebooks", type: "MyNotebooks",
} as Location, } as Location,
@@ -86,7 +86,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
options.push({ options.push({
key: "GitHub-Header", key: "GitHub-Header",
text: ResourceTreeAdapter.GitHubReposTitle, text: Notebook.GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header, itemType: SelectableOptionMenuItemType.Header,
}); });

View File

@@ -133,7 +133,7 @@ describe("Delete Collection Confirmation Pane", () => {
.simulate("change", { target: { value: selectedCollectionId } }); .simulate("change", { target: { value: selectedCollectionId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("click");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
wrapper.unmount(); wrapper.unmount();
@@ -154,7 +154,7 @@ describe("Delete Collection Confirmation Pane", () => {
.simulate("change", { target: { value: feedbackText } }); .simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("click");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
const deleteFeedback = new DeleteFeedback( const deleteFeedback = new DeleteFeedback(

View File

@@ -1,19 +1,20 @@
import { Text, TextField } from "office-ui-fabric-react"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as React from "react"; import * as React from "react";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { Collection } from "../../Contracts/ViewModels";
import { Text, TextField } from "office-ui-fabric-react";
import { userContext } from "../../UserContext";
import { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection } from "../../Contracts/ViewModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { PanelErrorComponent, PanelErrorProps } from "./PanelErrorComponent";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import DeleteFeedback from "../../Common/DeleteFeedback";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent"; import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
export interface DeleteCollectionConfirmationPanelProps { export interface DeleteCollectionConfirmationPanelProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
@@ -43,8 +44,8 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
render(): JSX.Element { render(): JSX.Element {
return ( return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}> <div className="panelContentContainer">
<PanelInfoErrorComponent {...this.getPanelErrorProps()} /> <PanelErrorComponent {...this.getPanelErrorProps()} />
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
@@ -78,16 +79,18 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
</div> </div>
)} )}
</div> </div>
<PanelFooterComponent buttonLabel="OK" /> <PanelFooterComponent buttonLabel="OK" onOKButtonClicked={() => this.submit()} />
{this.state.isExecuting && <PanelLoadingScreen />} <div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.state.isExecuting}>
</form> <img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
</div>
</div>
); );
} }
private getPanelErrorProps(): PanelInfoErrorProps { private getPanelErrorProps(): PanelErrorProps {
if (this.state.formError) { if (this.state.formError) {
return { return {
messageType: "error", isWarning: false,
message: this.state.formError, message: this.state.formError,
showErrorDetails: true, showErrorDetails: true,
openNotificationConsole: this.props.openNotificationConsole, openNotificationConsole: this.props.openNotificationConsole,
@@ -95,7 +98,7 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
} }
return { return {
messageType: "warning", isWarning: true,
showErrorDetails: false, showErrorDetails: false,
message: message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.", "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
@@ -106,10 +109,9 @@ export class DeleteCollectionConfirmationPanel extends React.Component<
return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared(); return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared();
} }
public async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> { public async submit(): Promise<void> {
event.preventDefault();
const collection = this.props.explorer.findSelectedCollection(); const collection = this.props.explorer.findSelectedCollection();
if (!collection || this.inputCollectionName !== collection.id()) { if (!collection || this.inputCollectionName !== collection.id()) {
const errorMessage = "Input collection name does not match the selected collection"; const errorMessage = "Input collection name does not match the selected collection";
this.setState({ formError: errorMessage }); this.setState({ formError: errorMessage });

View File

@@ -0,0 +1,109 @@
<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="deletedatabaseconfirmationpane">
<!-- Delete Databaes Confirmation form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Delete Database 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 Database 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 Database 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 Database Confirmation errors - End -->
<!-- Delete Database Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<span class="mandatoryStar">*</span> <span data-bind="text: databaseIdConfirmationText"></span>
<p>
<input
type="text"
name="databaseIdConfirmation"
data-test="confirmDatabaseId"
required
class="collid"
data-bind="value: databaseIdConfirmation, hasFocus: firstFieldHasFocus"
aria-label="Confirm by typing the database id"
/>
</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 database?</div>
<p>
<textarea
type="text"
data-test="databaseDeleteFeedback"
name="databaseDeleteFeedback"
rows="3"
cols="53"
maxlength="512"
class="collid"
data-bind="value: databaseDeleteFeedback"
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
>
</textarea>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="deleteDatabase" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Delete Database Confirmation inputs - End -->
</form>
</div>
<!-- Delete Database Confirmation form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,127 @@
jest.mock("../../Common/dataAccess/deleteDatabase");
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import * as ko from "knockout";
import Q from "q";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import DeleteDatabaseConfirmationPane from "./DeleteDatabaseConfirmationPane";
import DeleteFeedback from "../../Common/DeleteFeedback";
import Explorer from "../Explorer";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { TreeNode } from "../../Contracts/ViewModels";
import { TabsManager } from "../Tabs/TabsManager";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
describe("Delete Database Confirmation Pane", () => {
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
let explorer: Explorer;
beforeAll(() => {
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
});
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if only 1 database", () => {
let database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastDatabase()).toBe(true);
});
it("should be false if only 2 databases", () => {
let database = {} as ViewModels.Database;
let database2 = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
expect(explorer.isLastDatabase()).toBe(false);
});
it("should be false if not last empty database", () => {
let database = {} as ViewModels.Database;
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
let database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});
describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
let fakeExplorer = {} as Explorer;
let pane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any,
});
fakeExplorer.isLastNonEmptyDatabase = () => true;
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(true);
fakeExplorer.isLastDatabase = () => true;
fakeExplorer.isSelectedDatabaseShared = () => true;
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(true);
fakeExplorer.isLastNonEmptyDatabase = () => false;
fakeExplorer.isLastDatabase = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
pane.container = fakeExplorer as any;
expect(pane.shouldRecordFeedback()).toBe(false);
});
});
describe("submit()", () => {
it("on submit() it should log feedback if last non empty database or is last database that has shared throughput", () => {
let selectedDatabaseId = "testDB";
let fakeExplorer = {} as Explorer;
fakeExplorer.findSelectedDatabase = () => {
return {
id: ko.observable<string>(selectedDatabaseId),
rid: "test",
collections: ko.observableArray<ViewModels.Collection>(),
} as ViewModels.Database;
};
fakeExplorer.refreshAllDatabases = () => Q.resolve();
fakeExplorer.selectedDatabaseId = ko.computed<string>(() => selectedDatabaseId);
fakeExplorer.isSelectedDatabaseShared = () => false;
const SubscriptionId = "testId";
const AccountName = "testAccount";
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
id: SubscriptionId,
name: AccountName,
} as DataModels.DatabaseAccount);
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
fakeExplorer.isPreferredApiCassandra = ko.computed(() => {
return false;
});
fakeExplorer.selectedNode = ko.observable<TreeNode>();
fakeExplorer.tabsManager = new TabsManager();
fakeExplorer.isLastNonEmptyDatabase = () => true;
let pane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: fakeExplorer as any,
});
pane.databaseIdConfirmation = ko.observable<string>(selectedDatabaseId);
const Feedback = "my feedback";
pane.databaseDeleteFeedback(Feedback);
return pane.submit().then(() => {
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
});
});
});
});

View File

@@ -0,0 +1,143 @@
import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
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 { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import { ARMError } from "../../Utils/arm/request";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
public databaseIdConfirmationText: ko.Observable<string>;
public databaseIdConfirmation: ko.Observable<string>;
public databaseDeleteFeedback: ko.Observable<string>;
public recordDeleteFeedback: ko.Observable<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.databaseIdConfirmationText = ko.observable<string>("Confirm by typing the database id");
this.databaseIdConfirmation = ko.observable<string>();
this.databaseDeleteFeedback = ko.observable<string>();
this.recordDeleteFeedback = ko.observable<boolean>(false);
this.title("Delete Database");
this.resetData();
}
public submit(): Q.Promise<any> {
if (!this._isValid()) {
const selectedDatabase: ViewModels.Database = this.container.findSelectedDatabase();
this.formErrors("Input database name does not match the selected database");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}: ${this.formErrors()}`
);
return Q.resolve();
}
this.formErrors("");
this.isExecuting(true);
const selectedDatabase = this.container.findSelectedDatabase();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDatabase, {
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
});
return Q(
deleteDatabase(selectedDatabase.id()).then(
() => {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() &&
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
)
);
this.resetData();
TelemetryProcessor.traceSuccess(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.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.databaseDeleteFeedback()
);
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
this.databaseDeleteFeedback("");
}
},
(error: any) => {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
)
);
}
public resetData() {
this.databaseIdConfirmation("");
super.resetData();
}
public async open() {
await this.container.loadSelectedDatabaseOffer();
this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open();
}
public shouldRecordFeedback(): boolean {
return (
this.container.isLastNonEmptyDatabase() ||
(this.container.isLastDatabase() && this.container.isSelectedDatabaseShared())
);
}
private _isValid(): boolean {
const selectedDatabase = this.container.findSelectedDatabase();
if (!selectedDatabase) {
return false;
}
return this.databaseIdConfirmation() === selectedDatabase.id();
}
}

View File

@@ -1,139 +0,0 @@
jest.mock("../../Common/dataAccess/deleteDatabase");
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
import { Collection, Database } 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";
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
describe("Delete Database Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database.id = ko.observable<string>("testDatabse");
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
const wrapper = shallow(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
props.explorer.isLastNonEmptyDatabase = () => false;
props.explorer.isLastDatabase = () => false;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
props.explorer.isLastNonEmptyDatabase = () => false;
props.explorer.isLastDatabase = () => true;
props.explorer.isSelectedDatabaseShared = () => false;
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
});
});
describe("submit()", () => {
const selectedDatabaseId = "testDatabse";
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false;
let wrapper: ReactWrapper;
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "testDatabaseAccountName",
properties: {
cassandraEndpoint: "testEndpoint",
},
id: "testDatabaseAccountId",
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB,
});
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
});
beforeEach(() => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
});
it("Should call delete database", () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
wrapper.unmount();
});
it("should record feedback", async () => {
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
.hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#deleteDatabaseFeedbackInput")).toBe(true);
const feedbackText = "Test delete Database feedback text";
wrapper
.find("#deleteDatabaseFeedbackInput")
.hostNodes()
.simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
const deleteFeedback = new DeleteFeedback(
"testDatabaseAccountId",
"testDatabaseAccountName",
ApiKind.SQL,
feedbackText
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
wrapper.unmount();
});
});
});

View File

@@ -1,168 +0,0 @@
import { useBoolean } from "@uifabric/react-hooks";
import { Text, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../Common/Constants";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection, Database } 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 { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen";
interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer;
closePanel: () => void;
openNotificationConsole: () => void;
selectedDatabase: Database;
}
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = (
props: DeleteDatabaseConfirmationPanelProps
): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const getPanelErrorProps = (): PanelInfoErrorProps => {
if (formError) {
return {
messageType: "error",
message: formError,
showErrorDetails: true,
openNotificationConsole: 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.",
};
};
const submit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
const { selectedDatabase, explorer } = props;
event.preventDefault();
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError("Input database name does not match the selected database");
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`);
return;
}
setFormError("");
setLoadingTrue();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDatabase, {
databaseId: selectedDatabase.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Database",
});
try {
await deleteDatabase(selectedDatabase.id());
props.closePanel();
explorer.refreshAllDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
explorer.selectedNode(undefined);
selectedDatabase
.collections()
.forEach((collection: Collection) =>
explorer.tabsManager.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
)
);
TelemetryProcessor.traceSuccess(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Database",
},
startKey
);
if (shouldRecordFeedback()) {
const deleteFeedback = new DeleteFeedback(
userContext?.databaseAccount.id,
userContext?.databaseAccount.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
databaseFeedbackInput
);
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
}
} catch (error) {
setLoadingFalse();
setFormError(error);
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.DeleteDatabase,
{
databaseId: selectedDatabase.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle: "Delete Database",
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
};
const shouldRecordFeedback = (): boolean => {
const { explorer } = props;
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
};
return (
<form className="panelFormWrapper" onSubmit={submit}>
<PanelInfoErrorComponent {...getPanelErrorProps()} />
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the database id</Text>
<TextField
id="confirmDatabaseId"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
setDatabaseInput(newInput);
}}
/>
</div>
{shouldRecordFeedback() && (
<div className="deleteDatabaseFeedback">
<Text variant="small" block>
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
What is the reason why you are deleting this database?
</Text>
<TextField
id="deleteDatabaseFeedbackInput"
styles={{ fieldGroup: { width: 300 } }}
multiline
rows={3}
onChange={(event, newInput?: string) => {
setDatabaseFeedbackInput(newInput);
}}
/>
</div>
)}
</div>
<PanelFooterComponent buttonLabel="OK" />
{isLoading && <PanelLoadingScreen />}
</form>
);
};

View File

@@ -0,0 +1,175 @@
<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="executesprocparamspane">
<!-- Input params form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: execute">
<!-- Input params 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>
<!-- Input params header - End -->
<!-- Input params 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="
visible: formErrorsDetails() && formErrorsDetails() !== '',
click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Input params errors - End -->
<!-- Script for each param clause to be used for executing a stored procedure -->
<script type="text/html" id="param-template">
<tr>
<td class="paramTemplateRow">
<select class="dataTypeSelector" data-bind="value: type, attr: { 'aria-label': type }">
<option value="custom">Custom</option>
<option value="string">String</option>
</select>
</td>
<td class="paramTemplateRow">
<input class="valueTextBox" aria-label="Param" data-bind="textInput: value" />
<span
class="spEntityAddCancel"
data-bind="click: $parent.deleteParam.bind($parent, $index()), event: { keypress: $parent.onDeleteParamKeyPress.bind($parent, $index()) }"
role="button"
tabindex="0"
>
<img src="/Entity_cancel.svg" alt="Delete param" />
</span>
<span
class="spEntityAddCancel"
data-bind="click: $parent.addNewParamAtIndex.bind($parent, $index()), event: { keypress: $parent.onAddNewParamAtIndexKeyPress.bind($parent, $index()) }"
role="button"
tabindex="0"
>
<img src="/Add-property.svg" alt="Add param" />
</span>
</td>
</tr>
</script>
<!-- Input params input - Start -->
<div class="paneMainContent">
<div>
<!-- Partition key input - Start -->
<div class="partitionKeyContainer" data-bind="visible: collectionHasPartitionKey">
<div class="inputHeader">Partition key value</div>
<div class="scrollBox">
<table class="paramsClauseTable">
<thead>
<tr>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td class="paramTemplateRow">
<select
class="dataTypeSelector"
data-bind="value: partitionKeyType, attr: { 'aria-label': partitionKeyType }"
>
<option value="custom">Custom</option>
<option value="string">String</option>
</select>
</td>
<td class="paramTemplateRow">
<input
class="partitionKeyValue"
id="partitionKeyValue"
role="textbox"
tabindex="0"
aria-label="Partition key value"
data-bind="textInput: partitionKeyValue"
autofocus
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Partition key input - End -->
<!-- Input params table - Start -->
<div class="paramsTable">
<div class="enterInputParams">Enter input parameters (if any)</div>
<div class="scrollBox" id="executeSprocParamsScroll">
<table class="paramsClauseTable">
<thead>
<tr>
<th class="paramTableTypeHead">Type</th>
<th>Param</th>
</tr>
</thead>
<tbody data-bind="template: { name: 'param-template', foreach: params }"></tbody>
</table>
</div>
<div
id="addNewParamLink"
class="addNewParam"
data-bind="click: addNewParam, event: { keypress: onAddNewParamKeyPress }, attr:{ title: addNewParamLabel }"
role="button"
tabindex="0"
>
<span>
<img src="/Add-property.svg" alt="Add new param" />
<span class="addNewParamLabel" data-bind="text: addNewParamLabel" />
</span>
</div>
</div>
<!-- Input params table - End -->
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
type="submit"
value="Execute"
class="btncreatecoll1"
data-bind="{ css: { btnDisabled: !executeButtonEnabled() }}"
/>
</div>
</div>
<!-- Input param input - End -->
</form>
</div>
<!-- Input params form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,172 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import StoredProcedure from "../Tree/StoredProcedure";
export interface ExecuteSprocParam {
type: ko.Observable<string>;
value: ko.Observable<string>;
}
type UnwrappedExecuteSprocParam = {
type: string;
value: any;
};
export class ExecuteSprocParamsPane extends ContextualPaneBase {
public params: ko.ObservableArray<ExecuteSprocParam>;
public partitionKeyType: ko.Observable<string>;
public partitionKeyValue: ko.Observable<string>;
public collectionHasPartitionKey: ko.Observable<boolean>;
public addNewParamLabel: string = "Add New Param";
public executeButtonEnabled: ko.Computed<boolean>;
private _selectedSproc: StoredProcedure;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Input parameters");
this.partitionKeyType = ko.observable<string>("custom");
this.partitionKeyValue = ko.observable<string>();
this.executeButtonEnabled = ko.computed<boolean>(() => this.validPartitionKeyValue());
this.params = ko.observableArray<ExecuteSprocParam>([
{ type: ko.observable<string>("string"), value: ko.observable<string>() },
]);
this.collectionHasPartitionKey = ko.observable<boolean>();
this.resetData();
}
public open() {
super.open();
const currentSelectedSproc = this.container && this.container.findSelectedStoredProcedure();
if (!!currentSelectedSproc && !!this._selectedSproc && this._selectedSproc.rid !== currentSelectedSproc.rid) {
this.params([]);
this.partitionKeyValue("");
}
this._selectedSproc = currentSelectedSproc;
this.collectionHasPartitionKey((this.container && !!this.container.findSelectedCollection().partitionKey) || false);
const focusElement = document.getElementById("partitionKeyValue");
focusElement && focusElement.focus();
}
public execute = () => {
this.formErrors("");
const partitionKeyValue: string = (() => {
if (!this.collectionHasPartitionKey()) {
return undefined;
}
const type: string = this.partitionKeyType();
let value: string = this.partitionKeyValue();
if (type === "custom") {
if (value === "undefined" || value === undefined) {
return undefined;
}
if (value === "null" || value === null) {
return null;
}
try {
value = JSON.parse(value);
} catch (e) {
this.formErrors(`Invalid param specified: ${value}`);
this.formErrorsDetails(`Invalid param specified: ${value} is not a valid literal value`);
}
}
return value;
})();
const unwrappedParams: UnwrappedExecuteSprocParam[] = ko.toJS(this.params());
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = !this.params()
? undefined
: _.map(unwrappedParams, (unwrappedParam: UnwrappedExecuteSprocParam) => {
let paramValue: string = unwrappedParam.value;
if (unwrappedParam.type === "custom" && (paramValue === "undefined" || paramValue === "")) {
paramValue = undefined;
} else if (unwrappedParam.type === "custom") {
try {
paramValue = JSON.parse(paramValue);
} catch (e) {
this.formErrors(`Invalid param specified: ${paramValue}`);
this.formErrorsDetails(`Invalid param specified: ${paramValue} is not a valid literal value`);
}
}
unwrappedParam.value = paramValue;
return unwrappedParam;
});
if (this.formErrors()) {
return;
}
const sprocParams = wrappedSprocParams && _.pluck(wrappedSprocParams, "value");
this._selectedSproc.execute(sprocParams, partitionKeyValue);
this.close();
};
private validPartitionKeyValue = (): boolean => {
return !this.collectionHasPartitionKey || (this.partitionKeyValue() != null && this.partitionKeyValue().length > 0);
};
public addNewParam = (): void => {
this.params.push({ type: ko.observable<string>("string"), value: ko.observable<string>() });
this._maintainFocusOnAddNewParamLink();
};
public onAddNewParamKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.addNewParam();
event.stopPropagation();
return false;
}
return true;
};
public addNewParamAtIndex = (index: number): void => {
this.params.splice(index, 0, { type: ko.observable<string>("string"), value: ko.observable<string>() });
};
public onAddNewParamAtIndexKeyPress = (index: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.addNewParamAtIndex(index);
event.stopPropagation();
return false;
}
return true;
};
public deleteParam = (indexToRemove: number): void => {
const params = _.reject(this.params(), (param: ExecuteSprocParam, index: number) => {
return index === indexToRemove;
});
this.params(params);
};
public onDeleteParamKeyPress = (indexToRemove: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.deleteParam(indexToRemove);
event.stopPropagation();
return false;
}
return true;
};
public close(): void {
super.close();
this.formErrors("");
this.formErrorsDetails("");
}
private _maintainFocusOnAddNewParamLink(): void {
const addNewParamLink = document.getElementById("addNewParamLink");
addNewParamLink.scrollIntoView();
}
}

View File

@@ -1,91 +0,0 @@
import {
Dropdown,
IDropdownOption,
IDropdownStyles,
IImageProps,
Image,
Label,
Stack,
TextField,
} from "office-ui-fabric-react";
import React, { FunctionComponent } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import EntityCancelIcon from "../../../../images/Entity_cancel.svg";
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
const options = [
{ key: "string", text: "String" },
{ key: "custom", text: "Custom" },
];
export interface InputParameterProps {
dropdownLabel?: string;
inputParameterTitle?: string;
inputLabel?: string;
isAddRemoveVisible: boolean;
onDeleteParamKeyPress?: () => void;
onAddNewParamKeyPress?: () => void;
onParamValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onParamKeyChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption) => void;
paramValue: string;
selectedKey: string | number;
}
export const InputParameter: FunctionComponent<InputParameterProps> = ({
dropdownLabel,
inputParameterTitle,
inputLabel,
isAddRemoveVisible,
paramValue,
selectedKey,
onDeleteParamKeyPress,
onAddNewParamKeyPress,
onParamValueChange,
onParamKeyChange,
}: InputParameterProps): JSX.Element => {
const imageProps: IImageProps = {
width: 20,
height: 30,
className: dropdownLabel ? "addRemoveIconLabel" : "addRemoveIcon",
};
return (
<>
{inputParameterTitle && <Label>{inputParameterTitle}</Label>}
<Stack horizontal>
<Dropdown
label={dropdownLabel && dropdownLabel}
selectedKey={selectedKey}
onChange={onParamKeyChange}
options={options}
styles={dropdownStyles}
/>
<TextField
label={inputLabel && inputLabel}
id="confirmCollectionId"
autoFocus
value={paramValue}
onChange={onParamValueChange}
/>
{isAddRemoveVisible && (
<>
<Image
{...imageProps}
src={EntityCancelIcon}
alt="Delete param"
id="deleteparam"
onClick={onDeleteParamKeyPress}
/>
<Image
{...imageProps}
src={AddPropertyIcon}
alt="Add param"
id="addparam"
onClick={onAddNewParamKeyPress}
/>
</>
)}
</Stack>
</>
);
};

View File

@@ -1,34 +0,0 @@
import { mount } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { ExecuteSprocParamsPanel } from "./index";
describe("Excute Sproc Param Pane", () => {
const fakeExplorer = {} as Explorer;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
};
it("should render Default properly", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("initially display 2 input field, 1 partition and 1 parameter", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
expect(wrapper.find("input[type='text']")).toHaveLength(2);
});
it("add a new parameter field", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
wrapper.find("#addparam").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(3);
});
it("remove a parameter field", () => {
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
wrapper.find("#deleteparam").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(1);
});
});

View File

@@ -1,163 +0,0 @@
import { useBoolean } from "@uifabric/react-hooks";
import { IDropdownOption, IImageProps, Image, Stack, Text } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import Explorer from "../../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
import { InputParameter } from "./InputParameter";
interface ExecuteSprocParamsPaneProps {
explorer: Explorer;
closePanel: () => void;
}
const imageProps: IImageProps = {
width: 20,
height: 30,
};
interface UnwrappedExecuteSprocParam {
key: string;
text: string;
}
export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
explorer,
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 [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setSelectedKey(item);
};
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: formError,
formErrorDetail: formErrorsDetails,
id: "executesprocparamspane",
isExecuting: isLoading,
title: "Input parameters",
submitButtonText: "Execute",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const validateUnwrappedParams = (): boolean => {
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
for (let i = 0; i < unwrappedParams.length; i++) {
const { key: paramType, text: paramValue } = unwrappedParams[i];
if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) {
return false;
}
}
return true;
};
const setInvalidParamError = (invalidParam: string): void => {
setFormError(`Invalid param specified: ${invalidParam}`);
setFormErrorsDetails(`Invalid param specified: ${invalidParam} is not a valid literal value`);
};
const submit = (): void => {
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
const { key: partitionKey } = selectedKey;
if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) {
setInvalidParamError(partitionValue);
return;
}
if (!validateUnwrappedParams()) {
setInvalidParamError("");
return;
}
setLoadingTrue();
const sprocParams = wrappedSprocParams && wrappedSprocParams.map((sprocParam) => sprocParam.text);
const currentSelectedSproc = explorer.findSelectedStoredProcedure();
currentSelectedSproc.execute(sprocParams, partitionValue);
setLoadingFalse();
closePanel();
};
const deleteParamAtIndex = (indexToRemove: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToRemove, 1);
setParamKeyValues(cloneParamKeyValue);
};
const addNewParamAtIndex = (indexToAdd: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToAdd, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
};
const paramValueChange = (value: string, indexOfInput: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfInput].text = value;
setParamKeyValues(cloneParamKeyValue);
};
const paramKeyChange = (
_event: React.FormEvent<HTMLDivElement>,
selectedParam: IDropdownOption,
indexOfParam: number
): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfParam].key = selectedParam.key.toString();
setParamKeyValues(cloneParamKeyValue);
};
const addNewParamAtLastIndex = (): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(cloneParamKeyValue.length, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<InputParameter
dropdownLabel="Key"
inputParameterTitle="Partition key value"
inputLabel="Value"
isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => {
setPartitionValue(newInput);
}}
onParamKeyChange={onPartitionKeyChange}
paramValue={partitionValue}
selectedKey={selectedKey.key}
/>
{paramKeyValues.map((paramKeyValue, index) => (
<InputParameter
key={paramKeyValue && paramKeyValue.text + index}
dropdownLabel={!index && "Key"}
inputParameterTitle={!index && "Enter input parameters (if any)"}
inputLabel={!index && "Param"}
isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(index)}
onAddNewParamKeyPress={() => addNewParamAtIndex(index + 1)}
onParamValueChange={(event, newInput?: string) => {
paramValueChange(newInput, index);
}}
onParamKeyChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
paramKeyChange(event, selectedParam, index);
}}
paramValue={paramKeyValue && paramKeyValue.text}
selectedKey={paramKeyValue && paramKeyValue.key}
/>
))}
<Stack horizontal onClick={addNewParamAtLastIndex}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -1,9 +1,9 @@
import { Subscription } from "knockout";
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import * as React from "react"; import * as React from "react";
import { IconButton, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import { KeyCodes } from "../../Common/Constants";
import { Subscription } from "knockout";
import ErrorRedIcon from "../../../images/error_red.svg"; import ErrorRedIcon from "../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../Common/Constants";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
export interface GenericRightPaneProps { export interface GenericRightPaneProps {

View File

@@ -1,19 +1,24 @@
import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import AddDatabasePaneTemplate from "./AddDatabasePane.html"; import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import BrowseQueriesPaneTemplate from "./BrowseQueriesPane.html"; import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html"; import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html"; import DeleteDatabaseConfirmationPaneTemplate from "./DeleteDatabaseConfirmationPane.html";
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html"; import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html"; import GraphStylingPaneTemplate from "./GraphStylingPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableColumnOptionsPaneTemplate from "./Tables/TableColumnOptionsPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import SettingsPaneTemplate from "./SettingsPane.html";
import ExecuteSprocParamsPaneTemplate from "./ExecuteSprocParamsPane.html";
import UploadItemsPaneTemplate from "./UploadItemsPane.html";
import LoadQueryPaneTemplate from "./LoadQueryPane.html"; import LoadQueryPaneTemplate from "./LoadQueryPane.html";
import SaveQueryPaneTemplate from "./SaveQueryPane.html"; import SaveQueryPaneTemplate from "./SaveQueryPane.html";
import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html"; import BrowseQueriesPaneTemplate from "./BrowseQueriesPane.html";
import UploadFilePaneTemplate from "./UploadFilePane.html";
import StringInputPaneTemplate from "./StringInputPane.html"; import StringInputPaneTemplate from "./StringInputPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html"; import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
import TableColumnOptionsPaneTemplate from "./Tables/TableColumnOptionsPane.html"; import GitHubReposPaneTemplate from "./GitHubReposPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
export class PaneComponent { export class PaneComponent {
constructor(data: any) { constructor(data: any) {
@@ -48,6 +53,15 @@ export class DeleteCollectionConfirmationPaneComponent {
} }
} }
export class DeleteDatabaseConfirmationPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: DeleteDatabaseConfirmationPaneTemplate,
};
}
}
export class GraphNewVertexPaneComponent { export class GraphNewVertexPaneComponent {
constructor() { constructor() {
return { return {
@@ -111,6 +125,33 @@ export class CassandraAddCollectionPaneComponent {
} }
} }
export class SettingsPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: SettingsPaneTemplate,
};
}
}
export class ExecuteSprocParamsComponent {
constructor() {
return {
viewModel: PaneComponent,
template: ExecuteSprocParamsPaneTemplate,
};
}
}
export class UploadItemsPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: UploadItemsPaneTemplate,
};
}
}
export class LoadQueryPaneComponent { export class LoadQueryPaneComponent {
constructor() { constructor() {
return { return {
@@ -138,6 +179,15 @@ export class BrowseQueriesPaneComponent {
} }
} }
export class UploadFilePaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: UploadFilePaneTemplate,
};
}
}
export class StringInputPaneComponent { export class StringInputPaneComponent {
constructor() { constructor() {
return { return {

View File

@@ -1,58 +1,12 @@
@import "../../../less/Common/Constants"; @import "../../../less/Common/Constants";
.panelFormWrapper { .panelContentContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
.panelMainContent { .panelMainContent {
flex-grow: 1; flex-grow: 1;
padding: 0 34px;
margin: 20px 0;
overflow: auto;
& > * {
margin-bottom: @DefaultSpace;
& > * {
margin-bottom: @SmallSpace;
}
}
.panelInfoIcon {
font-size: @mediumFontSize;
width: @mediumFontSize;
margin: auto 0 auto @SmallSpace;
color: @InfoIconColor;
cursor: default;
vertical-align: middle;
}
.panelTextBold {
font-weight: 600;
line-height: 20px;
}
.panelTextField {
font-size: @mediumFontSize;
border: 1px solid #605e5c;
color: #000;
padding: 4px 10px;
width: @newCollectionPaneInputWidth;
}
.panelRadioBtn {
margin: 0;
}
.panelRadioBtnLabel {
font-size: @mediumFontSize;
padding: 0 @LargeSpace 0 @SmallSpace;
}
.collapsibleSection {
margin-bottom: 0;
}
} }
} }
@@ -62,30 +16,26 @@
font-weight: 400; font-weight: 400;
} }
.panelInfoErrorContainer { .panelWarningErrorContainer {
background-color: @BaseLow; background-color: @BaseLow;
padding: @DefaultSpace; padding: @DefaultSpace;
display: inline-flex; display: inline-flex;
margin: 20px 34px 0 34px; margin-bottom: 24px;
i {
font-size: @WarningErrorIconSize;
width: @WarningErrorIconSize;
margin-left: @SmallSpace;
}
.panelWarningIcon { .panelWarningIcon {
font-size: @WarningErrorIconSize;
width: @WarningErrorIconSize;
margin: auto 0 auto @SmallSpace;
color: @WarningIconColor; color: @WarningIconColor;
} }
.panelErrorIcon { .panelErrorIcon {
font-size: @WarningErrorIconSize;
width: @WarningErrorIconSize;
margin: auto 0 auto @SmallSpace;
color: @ErrorIconColor; color: @ErrorIconColor;
} }
.panelLargeInfoIcon {
color: @InfoIconColor;
}
.panelWarningErrorDetailsLinkContainer { .panelWarningErrorDetailsLinkContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -98,46 +48,10 @@
} }
} }
.panelFooter { .panelFooter button {
padding: 20px 34px; height: 30px;
border-top: solid 1px #bbbbbb;
& button {
height: 30px;
}
} }
.deleteCollectionFeedback { .deleteCollectionFeedback {
margin-top: 12px; margin-top: 12px;
} }
.addRemoveIcon {
margin-left: 4px !important;
}
.addRemoveIconLabel {
margin-top: 28px;
margin-left: 4px !important;
}
.addNewParamStyle {
margin-top: 5px;
margin-left: 5px !important;
cursor: pointer;
}
.panelGroupSpacing > * {
margin-bottom: @SmallSpace;
}
.panelAddIconLabel {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.panelAddIcon {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.removeIcon {
color: @InfoIconColor;
}

View File

@@ -9,30 +9,10 @@ export interface PanelContainerProps {
closePanel: () => void; closePanel: () => void;
} }
export interface PanelContainerState { export class PanelContainerComponent extends React.Component<PanelContainerProps> {
height: string;
}
export class PanelContainerComponent extends React.Component<PanelContainerProps, PanelContainerState> {
private static readonly consoleHeaderHeight = 32; private static readonly consoleHeaderHeight = 32;
private static readonly consoleContentHeight = 220; private static readonly consoleContentHeight = 220;
constructor(props: PanelContainerProps) {
super(props);
this.state = {
height: this.getPanelHeight(),
};
}
componentDidMount(): void {
window.addEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
}
componentWillUnmount(): void {
window.removeEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
}
render(): JSX.Element { render(): JSX.Element {
if (!this.props.panelContent) { if (!this.props.panelContent) {
return <></>; return <></>;
@@ -50,10 +30,8 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
headerClassName="panelHeader" headerClassName="panelHeader"
styles={{ styles={{
navigation: { borderBottom: "1px solid #cccccc" }, navigation: { borderBottom: "1px solid #cccccc" },
content: { padding: 0, height: "100%" }, content: { padding: "24px 34px 20px 34px", height: "100%" },
scrollableContent: { height: "100%" }, scrollableContent: { height: "100%" },
header: { padding: "0 0 8px 34px" },
commands: { marginTop: 8 },
}} }}
style={{ height: this.getPanelHeight() }} style={{ height: this.getPanelHeight() }}
> >

View File

@@ -0,0 +1,29 @@
import React from "react";
import { Icon, Text } from "office-ui-fabric-react";
export interface PanelErrorProps {
message: string;
isWarning: boolean;
showErrorDetails: boolean;
openNotificationConsole?: () => void;
}
export const PanelErrorComponent: React.FunctionComponent<PanelErrorProps> = (props: PanelErrorProps): JSX.Element => (
<div className="panelWarningErrorContainer">
{props.isWarning ? (
<Icon iconName="WarningSolid" className="panelWarningIcon" />
) : (
<Icon iconName="StatusErrorFull" className="panelErrorIcon" />
)}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small">
{props.message}
</Text>
{props.showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={props.openNotificationConsole}>
More details
</a>
)}
</span>
</div>
);

View File

@@ -3,12 +3,13 @@ import { PrimaryButton } from "office-ui-fabric-react";
export interface PanelFooterProps { export interface PanelFooterProps {
buttonLabel: string; buttonLabel: string;
onOKButtonClicked: () => void;
} }
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ( export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
props: PanelFooterProps props: PanelFooterProps
): JSX.Element => ( ): JSX.Element => (
<div className="panelFooter"> <div className="panelFooter">
<PrimaryButton type="submit" id="sidePanelOkButton" text={props.buttonLabel} /> <PrimaryButton id="sidePanelOkButton" text={props.buttonLabel} onClick={() => props.onOKButtonClicked()} />
</div> </div>
); );

View File

@@ -1,45 +0,0 @@
import React from "react";
import { Icon, Link, Stack, Text } from "office-ui-fabric-react";
export interface PanelInfoErrorProps {
message: string;
messageType: string;
showErrorDetails: boolean;
link?: string;
linkText?: string;
openNotificationConsole?: () => void;
}
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = (
props: PanelInfoErrorProps
): JSX.Element => {
let icon: JSX.Element;
if (props.messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" />;
} else if (props.messageType === "warning") {
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" />;
} else if (props.messageType === "info") {
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" />;
}
return (
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="start">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small">
{props.message}{" "}
{props.link && props.linkText && (
<Link target="_blank" href={props.link}>
{props.linkText}
</Link>
)}
</Text>
{props.showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={props.openNotificationConsole}>
More details
</a>
)}
</span>
</Stack>
);
};

View File

@@ -1,8 +0,0 @@
import React from "react";
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
export const PanelLoadingScreen: React.FunctionComponent = () => (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
</div>
);

View File

@@ -14,7 +14,7 @@ import { handleError, getErrorMessage, getErrorStack } from "../../Common/ErrorH
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as FileSystemUtil from "../Notebook/FileSystemUtil"; import { FileSystemUtil } from "../Notebook/FileSystemUtil";
export class PublishNotebookPaneAdapter implements ReactAdapter { export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>; parameters: ko.Observable<number>;

View File

@@ -1,7 +1,7 @@
import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react"; import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent"; import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
import * as FileSystemUtil from "../Notebook/FileSystemUtil"; import { FileSystemUtil } from "../Notebook/FileSystemUtil";
import "./PublishNotebookPaneComponent.less"; import "./PublishNotebookPaneComponent.less";
import Html2Canvas from "html2canvas"; import Html2Canvas from "html2canvas";
import { ImmutableNotebook } from "@nteract/commutable/src"; import { ImmutableNotebook } from "@nteract/commutable/src";

View File

@@ -0,0 +1,268 @@
<!-- TODO: Move Pane to REACT -->
<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="settingspane">
<!-- Settings Confirmation form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Settings 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: { keydown: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Settings Confirmation header - End -->
<!-- Settings 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="
visible: formErrorsDetails() && formErrorsDetails() !== '',
click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Settings Confirmation errors - End -->
<!-- Settings Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<div class="settingsSection" data-bind="visible: shouldShowQueryPageOptions">
<div class="settingsSectionPart pageOptionsPart">
<div class="settingsSectionLabel">
Page options
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
many query results per page.
</span>
</span>
</div>
<div class="tabs" role="radiogroup" aria-label="Page options">
<!-- Fixed option button - Start -->
<div class="tab">
<input
type="radio"
id="customItemPerPage"
name="pageOption"
value="custom"
data-bind="checked: pageOption"
/>
<label
for="customItemPerPage"
id="custom-selection"
tabindex="0"
role="radio"
data-bind=" attr:{
'aria-checked': pageOption() === 'custom' ? 'true' : 'false' },
event: { keydown: onCustomPageOptionsKeyDown
}"
>Custom</label
>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input
type="radio"
id="unlimitedItemPerPage"
name="pageOption"
value="unlimited"
data-bind="checked: pageOption"
/>
<label
for="unlimitedItemPerPage"
id="unlimited-selection"
tabindex="0"
role="radio"
data-bind=" attr:{
'aria-checked': pageOption() === 'unlimited' ? 'true' : 'false' },
event: { keydown: onUnlimitedPageOptionKeyDown
}"
>Unlimited</label
>
</div>
<!-- Unlimited option button - End -->
</div>
</div>
<div class="tabs settingsSectionPart">
<div class="tabcontent" data-bind="visible: isCustomPageOptionSelected()">
<div class="settingsSectionLabel">
Query results per page
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Enter the number of query results that should be shown per page.
</span>
</span>
</div>
<input
type="number"
required
min="1"
step="1"
class="textfontclr collid"
aria-label="Custom query items per page"
data-bind="textInput: customItemPerPage, enable: isCustomPageOptionSelected()"
/>
</div>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowCrossPartitionOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Enable cross-partition query
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Send more than one request while executing a query. More than one request is necessary if the
query is not scoped to single partition key value.
</span>
</span>
</div>
<input
type="checkbox"
tabindex="0"
aria-label="Enable cross partition query"
data-bind="checked: crossPartitionQueryEnabled"
/>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowParallelismOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Max degree of parallelism
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Gets or sets the number of concurrent operations run client side during parallel query execution.
A positive property value limits the number of concurrent operations to the set value. If it is
set to less than 0, the system automatically decides the number of concurrent operations to run.
</span>
</span>
</div>
<input
type="number"
required
min="-1"
step="1"
class="textfontclr collid"
role="textbox"
tabindex="0"
id="max-degree"
aria-label="Max degree of parallelism"
data-bind="value: maxDegreeOfParallelism"
autofocus
/>
</div>
</div>
<div class="settingsSection" data-bind="visible: shouldShowGraphAutoVizOption">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">
Display Gremlin query results as:&nbsp;
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext pageOptionTooltipWidth">
Select Graph to automatically visualize the query results as a Graph or JSON to display the
results as JSON.
</span>
</span>
</div>
<div class="tabs" role="radiogroup" aria-label="Graph Auto-visualization">
<!-- Fixed option button - Start -->
<div class="tab">
<input
type="radio"
id="graphAutoVizOn"
name="graphAutoVizOption"
value="false"
data-bind="checked: graphAutoVizDisabled"
/>
<label
for="graphAutoVizOn"
id="graph-display"
tabindex="0"
role="radio"
data-bind="
attr: { 'aria-checked': graphAutoVizDisabled() === 'false' ? 'true' : 'false' },
event: { keypress: onGraphDisplayResultsKeyDown
}"
>Graph</label
>
</div>
<!-- Fixed option button - End -->
<!-- Unlimited option button - Start -->
<div class="tab">
<input
type="radio"
id="graphAutoVizOff"
name="graphAutoVizOption"
value="true"
data-bind="checked: graphAutoVizDisabled"
/>
<label
for="graphAutoVizOff"
tabindex="0"
role="radio"
data-bind="
attr: { 'aria-checked': graphAutoVizDisabled() === 'true' ? 'true' : 'false' },
event: { keypress: onJsonDisplayResultsKeyDown
}"
>JSON</label
>
</div>
<!-- Unlimited option button - End -->
</div>
</div>
</div>
<div class="settingsSection">
<div class="settingsSectionPart">
<div class="settingsSectionLabel">Explorer Version</div>
<div data-bind="text: explorerVersion"></div>
</div>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Apply" class="btncreatecoll1" /></div>
</div>
<!-- Settings Confirmation inputs - End -->
</form>
</div>
<!-- Settings Confirmation form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,38 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
describe("Settings Pane", () => {
describe("shouldShowQueryPageOptions()", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
});
it("should be true for SQL API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.DocumentDB.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(true);
});
it("should be false for Cassandra API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Cassandra.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Tables API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Table.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Graph API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.Graph.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
it("should be false for Mongo API", () => {
explorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB.toLowerCase());
expect(explorer.settingsPane.shouldShowQueryPageOptions()).toBe(false);
});
});
});

View File

@@ -0,0 +1,185 @@
import * as Constants from "../../Common/Constants";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as StringUtility from "../../Shared/StringUtility";
import { configContext } from "../../ConfigContext";
export class SettingsPane extends ContextualPaneBase {
public pageOption: ko.Observable<string>;
public customItemPerPage: ko.Observable<number>;
public crossPartitionQueryEnabled: ko.Observable<boolean>;
public maxDegreeOfParallelism: ko.Observable<number>;
public explorerVersion: string;
public shouldShowQueryPageOptions: ko.Computed<boolean>;
public shouldShowGraphAutoVizOption: ko.Computed<boolean>;
private graphAutoVizDisabled: ko.Observable<string>;
private shouldShowCrossPartitionOption: ko.Computed<boolean>;
private shouldShowParallelismOption: ko.Computed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Settings");
this.resetData();
this.pageOption = ko.observable<string>();
this.customItemPerPage = ko.observable<number>();
const crossPartitionQueryEnabledState: boolean = LocalStorageUtility.hasItem(
StorageKey.IsCrossPartitionQueryEnabled
)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
: true;
this.crossPartitionQueryEnabled = ko.observable<boolean>(crossPartitionQueryEnabledState);
const maxDegreeOfParallelismState: number = LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
: Constants.Queries.DefaultMaxDegreeOfParallelism;
this.maxDegreeOfParallelism = ko.observable<number>(maxDegreeOfParallelismState);
const isGraphAutoVizDisabled: boolean = LocalStorageUtility.hasItem(StorageKey.IsGraphAutoVizDisabled)
? LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled)
: false;
this.graphAutoVizDisabled = ko.observable<string>(`${isGraphAutoVizDisabled}`);
this.explorerVersion = configContext.gitSha;
this.shouldShowQueryPageOptions = ko.computed<boolean>(() => this.container.isPreferredApiDocumentDB());
this.shouldShowCrossPartitionOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
this.shouldShowParallelismOption = ko.computed<boolean>(() => !this.container.isPreferredApiGraph());
this.shouldShowGraphAutoVizOption = ko.computed<boolean>(() => this.container.isPreferredApiGraph());
}
public open() {
this._loadSettings();
super.open();
const pageOptionsFocus = document.getElementById("custom-selection");
const displayQueryFocus = document.getElementById("graph-display");
const maxDegreeFocus = document.getElementById("max-degree");
if (this.container.isPreferredApiGraph()) {
displayQueryFocus && displayQueryFocus.focus();
} else if (this.container.isPreferredApiTable()) {
maxDegreeFocus && maxDegreeFocus.focus();
}
pageOptionsFocus && pageOptionsFocus.focus();
}
public submit() {
this.formErrors("");
this.isExecuting(true);
LocalStorageUtility.setEntryNumber(
StorageKey.ActualItemPerPage,
this.isCustomPageOptionSelected() ? this.customItemPerPage() : Constants.Queries.unlimitedItemsPerPage
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, this.customItemPerPage());
LocalStorageUtility.setEntryString(
StorageKey.IsCrossPartitionQueryEnabled,
this.crossPartitionQueryEnabled().toString()
);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, this.maxDegreeOfParallelism());
if (this.shouldShowGraphAutoVizOption()) {
LocalStorageUtility.setEntryBoolean(
StorageKey.IsGraphAutoVizDisabled,
StringUtility.toBoolean(this.graphAutoVizDisabled())
);
}
this.isExecuting(false);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`${this.crossPartitionQueryEnabled() ? "Enabled" : "Disabled"} cross-partition query feed option`
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber(
StorageKey.MaxDegreeOfParellism
)}`
);
if (this.shouldShowGraphAutoVizOption()) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Graph result will be displayed as ${
LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled) ? "JSON" : "Graph"
}`
);
}
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`
);
this.close();
}
public isCustomPageOptionSelected = (): boolean => {
return this.pageOption() === Constants.Queries.CustomPageOption;
};
public isUnlimitedPageOptionSelected = (): boolean => {
return this.pageOption() === Constants.Queries.UnlimitedPageOption;
};
public onUnlimitedPageOptionKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.pageOption(Constants.Queries.UnlimitedPageOption);
event.stopPropagation();
return false;
}
return true;
}
public onCustomPageOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.pageOption(Constants.Queries.CustomPageOption);
event.stopPropagation();
return false;
}
return true;
}
public onJsonDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.graphAutoVizDisabled("true");
event.stopPropagation();
return false;
}
return true;
}
public onGraphDisplayResultsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.graphAutoVizDisabled("false");
event.stopPropagation();
return false;
}
return true;
}
private _loadSettings() {
this.isExecuting(true);
try {
this.pageOption(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) == Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption
);
this.customItemPerPage(LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage));
} catch (exception) {
this.formErrors("Unable to load your settings");
this.formErrorsDetails(exception);
} finally {
this.isExecuting(false);
}
}
}

View File

@@ -1,28 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { SettingsPane } from ".";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,
};
describe("Settings Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<SettingsPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("should render Gremlin properly", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const wrapper = shallow(<SettingsPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,253 +0,0 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "office-ui-fabric-react";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { Tooltip } from "../../../Common/Tooltip";
import { configContext } from "../../../ConfigContext";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import * as StringUtility from "../../../Shared/StringUtility";
import { userContext } from "../../../UserContext";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
export interface SettingsPaneProps {
explorer: Explorer;
closePanel: () => void;
}
export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
explorer: container,
closePanel,
}: SettingsPaneProps) => {
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [pageOption, setPageOption] = useState<string>(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption
);
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
);
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
: true
);
const [graphAutoVizDisabled, setGraphAutoVizDisabled] = useState<string>(
LocalStorageUtility.hasItem(StorageKey.IsGraphAutoVizDisabled)
? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled)
: "false"
);
const [maxDegreeOfParallelism, setMaxDegreeOfParallelism] = useState<number>(
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
: Constants.Queries.DefaultMaxDegreeOfParallelism
);
const explorerVersion = configContext.gitSha;
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
setFormErrors("");
setIsExecuting(true);
LocalStorageUtility.setEntryNumber(
StorageKey.ActualItemPerPage,
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
);
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean(
StorageKey.IsGraphAutoVizDisabled,
StringUtility.toBoolean(graphAutoVizDisabled)
);
}
setIsExecuting(false);
logConsoleInfo(
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`
);
logConsoleInfo(`${crossPartitionQueryEnabled ? "Enabled" : "Disabled"} cross-partition query feed option`);
logConsoleInfo(
`Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber(
StorageKey.MaxDegreeOfParellism
)}`
);
if (shouldShowGraphAutoVizOption) {
logConsoleInfo(
`Graph result will be displayed as ${
LocalStorageUtility.getEntryBoolean(StorageKey.IsGraphAutoVizDisabled) ? "JSON" : "Graph"
}`
);
}
logConsoleInfo(
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`
);
closePanel();
e.preventDefault();
};
const isCustomPageOptionSelected = () => {
return pageOption === Constants.Queries.CustomPageOption;
};
const handleOnGremlinChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setGraphAutoVizDisabled(option.key);
};
const genericPaneProps: GenericRightPaneProps = {
container,
formError: formErrors,
formErrorDetail: "",
id: "settingspane",
isExecuting,
title: "Setting",
submitButtonText: "Apply",
onClose: () => closePanel(),
onSubmit: () => handlerOnSubmit(undefined),
};
const pageOptionList: IChoiceGroupOption[] = [
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
{ key: Constants.Queries.UnlimitedPageOption, text: "Unlimited" },
];
const graphAutoOptionList: IChoiceGroupOption[] = [
{ key: "false", text: "Graph" },
{ key: "true", text: "JSON" },
];
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setPageOption(option.key);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="paneMainContent">
{shouldShowQueryPageOptions && (
<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>
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
</div>
<div className="tabs settingsSectionPart">
{isCustomPageOptionSelected() && (
<div className="tabcontent">
<div className="settingsSectionLabel">
Query results per page
<Tooltip>Enter the number of query results that should be shown per page.</Tooltip>
</div>
<SpinButton
ariaLabel="Custom query items per page"
value={"" + customItemPerPage}
onIncrement={(newValue) => {
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
}}
onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)}
onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)}
min={1}
step={1}
className="textfontclr"
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
/>
</div>
)}
</div>
</div>
)}
{shouldShowCrossPartitionOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Enable cross-partition query
<Tooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</Tooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
tabIndex={0}
ariaLabel="Enable cross partition query"
checked={crossPartitionQueryEnabled}
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
/>
</div>
</div>
)}
{shouldShowParallelismOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Max degree of parallelism
<Tooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</Tooltip>
</div>
<SpinButton
min={-1}
step={1}
className="textfontclr"
role="textbox"
tabIndex={0}
id="max-degree"
value={"" + maxDegreeOfParallelism}
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
onDecrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)}
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
ariaLabel="Max degree of parallelism"
/>
</div>
</div>
)}
{shouldShowGraphAutoVizOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Display Gremlin query results as:&nbsp;
<Tooltip>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as
JSON.
</Tooltip>
</div>
<ChoiceGroup
selectedKey={graphAutoVizDisabled}
options={graphAutoOptionList}
onChange={handleOnGremlinChange}
aria-label="Graph Auto-visualization"
/>
</div>
</div>
)}
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">Explorer Version</div>
<div>{explorerVersion}</div>
</div>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -0,0 +1,83 @@
<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="uploadFilePane">
<!-- Upload File form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload File 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">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Upload File header - End -->
<!-- Upload File 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="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Upload File errors - End -->
<!-- Upload File inputs - Start -->
<div class="paneMainContent">
<div>
<div class="renewUploadItemsHeader" data-bind="text: selectFileInputLabel"></div>
<input class="importFilesTitle" type="text" disabled data-bind="value: selectedFilesTitle" />
<input
type="file"
id="importFileInput"
style="display: none"
data-bind="event: { change: updateSelectedFiles }, attr: { accept: extensions }"
/>
<a
href="#"
id="fileImportLinkNotebook"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
>
<img
id="importFileButton"
class="fileImportImg"
src="/folder_16x16.svg"
alt="upload files"
title="Upload files"
/>
</a>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
id="uploadFileButton"
type="submit"
data-bind="attr: { value: submitButtonLabel }"
class="btncreatecoll1"
/>
</div>
</div>
<!-- Upload File inputs - End -->
</form>
</div>
<!-- Upload File form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,137 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
export interface UploadFilePaneOpenOptions {
paneTitle: string;
selectFileInputLabel: string;
errorMessage: string; // Could not upload notebook
inProgressMessage: string; // Uploading notebook
successMessage: string; // Successfully uploaded notebook
onSubmit: (file: File) => Promise<any>;
extensions?: string; // input accept field. E.g: .ipynb
submitButtonLabel?: string;
}
export class UploadFilePane extends ContextualPaneBase {
public selectedFilesTitle: ko.Observable<string>;
public files: ko.Observable<FileList>;
private openOptions: UploadFilePaneOpenOptions;
private submitButtonLabel: ko.Observable<string>;
private selectFileInputLabel: ko.Observable<string>;
private extensions: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.resetData();
this.selectFileInputLabel = ko.observable("");
this.selectedFilesTitle = ko.observable<string>("");
this.extensions = ko.observable(null);
this.submitButtonLabel = ko.observable("Load");
this.files = ko.observable<FileList>();
this.files.subscribe((newFiles: FileList) => this.updateSelectedFilesTitle(newFiles));
}
public submit() {
this.formErrors("");
this.formErrorsDetails("");
if (!this.files() || this.files().length === 0) {
this.formErrors("No file specified");
this.formErrorsDetails("No file specified. Please input a file.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`${this.openOptions.errorMessage} -- No file specified. Please input a file.`
);
return;
}
const file: File = this.files().item(0);
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`${this.openOptions.inProgressMessage}: ${file.name}`
);
this.isExecuting(true);
this.openOptions
.onSubmit(this.files().item(0))
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`${this.openOptions.successMessage} ${file.name}`
);
this.close();
},
(error: any) => {
this.formErrors(this.openOptions.errorMessage);
this.formErrorsDetails(`${this.openOptions.errorMessage}: ${error}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`${this.openOptions.errorMessage} ${file.name}: ${error}`
);
}
)
.finally(() => {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
}
public updateSelectedFiles(element: any, event: any): void {
this.files(event.target.files);
}
public close() {
super.close();
this.resetData();
this.files(undefined);
this.resetFileInput();
}
public openWithOptions(options: UploadFilePaneOpenOptions): void {
this.openOptions = options;
this.title(this.openOptions.paneTitle);
if (this.openOptions.submitButtonLabel) {
this.submitButtonLabel(this.openOptions.submitButtonLabel);
}
this.selectFileInputLabel(this.openOptions.selectFileInputLabel);
if (this.openOptions.extensions) {
this.extensions(this.openOptions.extensions);
}
super.open();
}
public onImportLinkClick(source: any, event: MouseEvent): boolean {
document.getElementById("importFileInput").click();
return false;
}
public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onImportLinkClick(source, null);
return false;
}
return true;
};
private updateSelectedFilesTitle(fileList: FileList) {
this.selectedFilesTitle("");
if (!fileList || fileList.length === 0) {
return;
}
for (let i = 0; i < fileList.length; i++) {
const originalTitle = this.selectedFilesTitle();
this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`);
}
}
private resetFileInput(): void {
const inputElement = $("#importFileInput");
inputElement.wrap("<form>").closest("form").get(0).reset();
inputElement.unwrap();
}
}

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