resolve conflict master branch

This commit is contained in:
hardiknai-techm
2021-04-23 19:13:13 +05:30
193 changed files with 15107 additions and 6742 deletions

View File

@@ -90,8 +90,7 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts
src/Explorer/Graph/NewVertexComponent/NewVertex.test.ts
src/Explorer/Graph/NewVertexComponent/NewVertexComponent.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts 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
@@ -123,13 +122,11 @@ src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/GraphStylingPane.ts src/Explorer/Panes/GraphStylingPane.ts
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/SetupNotebooksPane.ts
src/Explorer/Panes/StringInputPane.ts src/Explorer/Panes/StringInputPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/AddTableEntityPane.ts
src/Explorer/Panes/Tables/EditTableEntityPane.ts src/Explorer/Panes/Tables/EditTableEntityPane.ts
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
src/Explorer/Panes/Tables/TableEntityPane.ts src/Explorer/Panes/Tables/TableEntityPane.ts

View File

@@ -101,7 +101,8 @@ jobs:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
endtoendemulator: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Emulator Tests"
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') # Temporarily disabled. This test needs to be rewritten in playwright
if: false
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -126,58 +127,21 @@ jobs:
with: with:
name: screenshots name: screenshots
path: failed-* path: failed-*
accessibility: endtoend:
name: "Accessibility | Hosted" name: "E2E"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Accessibility Check
run: |
# Ubuntu gets mad when webpack runs too many files watchers
cat /proc/sys/fs/inotify/max_user_watches
sudo sysctl fs.inotify.max_user_watches=524288
sudo sysctl -p
npm ci
npm start &
npx wait-on -i 5000 https-get://0.0.0.0:1234/
node utils/accesibilityCheck.js
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendhosted:
name: "End to End Tests"
needs: [cleanupaccounts]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY }}
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
test-file: test-file:
- ./test/cassandra/container.spec.ts - ./test/cassandra/container.spec.ts
- ./test/mongo/mongoIndexPolicy.spec.ts
- ./test/notebooks/uploadAndOpenNotebook.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
- ./test/sql/container.spec.ts - ./test/sql/container.spec.ts
- ./test/mongo/container.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
- ./test/notebooks/upload.spec.ts
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:
@@ -188,30 +152,17 @@ jobs:
node-version: 14.x node-version: 14.x
- run: npm ci - run: npm ci
- run: npm start & - run: npm start &
- run: node utils/cleanupDBs.js
- run: npm run wait-for-server - run: npm run wait-for-server
- name: ${{ matrix['test-file'] }} - name: ${{ matrix['test-file'] }}
run: npx jest -c ./jest.config.e2e.js --detectOpenHandles ${{ matrix['test-file'] }} run: |
# Run tests up to three times
for i in $(seq 1 3); do npx jest -c ./jest.config.playwright.js ${{ matrix['test-file'] }} && s=0 && break || s=$? && sleep 1; done; (exit $s)
shell: bash shell: bash
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure() if: failure()
with: with:
name: screenshots name: screenshots
path: failed-* path: screenshots/
cleanupaccounts:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: npm ci
- run: node utils/cleanupDBs.js
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')

28
.github/workflows/cleanup.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
# This is a basic workflow to help you get started with Actions
name: Cleanup End to End Account Resources
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
schedule:
# Once every hour
- cron: "0 * * * *"
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
cleanupaccounts:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: npm ci
- run: node utils/cleanupDBs.js

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ Contracts/*
.cache/ .cache/
.env .env
failure.png failure.png
screenshots/*
GettingStarted-ignore*.ipynb

View File

@@ -153,7 +153,7 @@ Cosmos Explorer has been under constant development for over 5 years. As a resul
✅ DO ✅ DO
- Use [Puppeteer](https://developers.google.com/web/tools/puppeteer) and [Jest](https://jestjs.io/) - Use [Playwright](https://github.com/microsoft/playwright) and [Jest](https://jestjs.io/)
- Write or modify an existing E2E test that covers the primary use case of any major feature. - Write or modify an existing E2E test that covers the primary use case of any major feature.
- Use caution. Do not try to cover every case. End to End tests can be slow and brittle. - Use caution. Do not try to cover every case. End to End tests can be slow and brittle.

File diff suppressed because one or more lines are too long

13
jest-playwright.config.js Normal file
View File

@@ -0,0 +1,13 @@
const isCI = require("is-ci");
module.exports = {
exitOnPageError: false,
launchOptions: {
headless: isCI,
slowMo: 10,
timeout: 60000,
},
contextOptions: {
ignoreHTTPSErrors: true,
},
};

View File

@@ -1,12 +0,0 @@
const isCI = require("is-ci");
module.exports = {
launch: {
headless: isCI,
slowMo: 55,
defaultViewport: null,
ignoreHTTPSErrors: true,
args: ["--disable-web-security"],
exitOnPageError: false,
},
};

View File

@@ -1,5 +0,0 @@
module.exports = {
preset: "jest-puppeteer",
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
setupFiles: ["dotenv/config"],
};

View File

@@ -0,0 +1,7 @@
module.exports = {
preset: "jest-playwright-preset",
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
setupFiles: ["dotenv/config"],
testEnvironment: "./test/playwrightEnv.js",
setupFilesAfterEnv: ["expect-playwright"],
};

4569
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,7 @@
"i18next": "19.8.4", "i18next": "19.8.4",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23", "i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.5.1", "jquery": "3.5.1",
@@ -78,6 +79,7 @@
"office-ui-fabric-react": "7.164.2", "office-ui-fabric-react": "7.164.2",
"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",
"post-robot": "10.0.42",
"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",
@@ -111,15 +113,12 @@
"@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/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/memoize-one": "4.1.1", "@types/memoize-one": "4.1.1",
"@types/node": "12.11.1", "@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/promise.prototype.finally": "2.0.3", "@types/promise.prototype.finally": "2.0.3",
"@types/prop-types": "15.5.8",
"@types/puppeteer": "5.4.3",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.3", "@types/react": "17.0.3",
"@types/react-dom": "17.0.3", "@types/react-dom": "17.0.3",
@@ -131,7 +130,6 @@
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@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",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"buffer": "5.1.0", "buffer": "5.1.0",
@@ -146,16 +144,18 @@
"eslint-plugin-no-null": "1.0.2", "eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2", "eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react-hooks": "4.2.0", "eslint-plugin-react-hooks": "4.2.0",
"expect-playwright": "0.3.3",
"expose-loader": "0.7.5", "expose-loader": "0.7.5",
"fast-glob": "3.2.5", "fast-glob": "3.2.5",
"file-loader": "2.0.0", "file-loader": "2.0.0",
"fs-extra": "7.0.0", "fs-extra": "7.0.0",
"html-inline-css-webpack-plugin": "1.11.0",
"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": "4.5.2",
"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-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
@@ -163,16 +163,17 @@
"mini-css-extract-plugin": "0.4.3", "mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"playwright": "1.10.0",
"prettier": "2.2.1", "prettier": "2.2.1",
"puppeteer": "8.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4",
"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",
"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",
"typescript": "4.2.3", "typescript": "4.2.4",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "4.43.0", "webpack": "4.43.0",

View File

@@ -57,8 +57,6 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
const portal = new URL("https://ms.portal.azure.com/"); const portal = new URL("https://ms.portal.azure.com/");
portal.searchParams.set("dataExplorerSource", explorer.href); portal.searchParams.set("dataExplorerSource", explorer.href);
portal.hash =
"@microsoft.onmicrosoft.com/resource/subscriptions/b9c77f10-b438-4c32-9819-eef8a654e478/resourceGroups/stfaul/providers/Microsoft.DocumentDb/databaseAccounts/stfaul-sql/dataExplorer";
return res.redirect(portal.href); return res.redirect(portal.href);
}) })

View File

@@ -0,0 +1,68 @@
import { createImmutableOutput, JSONObject, OnDiskOutput } from "@nteract/commutable";
// import outputs individually to avoid increasing the bundle size
import { KernelOutputError } from "@nteract/outputs/lib/components/kernel-output-error";
import { Output } from "@nteract/outputs/lib/components/output";
import { StreamText } from "@nteract/outputs/lib/components/stream-text";
import { ContentRef } from "@nteract/types";
import "bootstrap/dist/css/bootstrap.css";
import postRobot from "post-robot";
import * as React from "react";
import * as ReactDOM from "react-dom";
import "../../externals/iframeResizer.contentWindow.min.js"; // Required for iFrameResizer to work
import "../Explorer/Notebook/NotebookRenderer/base.css";
import "../Explorer/Notebook/NotebookRenderer/default.css";
import { TransformMedia } from "./TransformMedia";
export interface CellOutputViewerProps {
id: string;
contentRef: ContentRef;
hidden: boolean;
expanded: boolean;
outputs: OnDiskOutput[];
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => void;
}
const onInit = async () => {
postRobot.on(
"props",
{
window: window.parent,
domain: window.location.origin,
},
(event) => {
// Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = (event as any).data as CellOutputViewerProps;
const outputs = (
<div
data-iframe-height
className={`nteract-cell-outputs ${props.hidden ? "hidden" : ""} ${props.expanded ? "expanded" : ""}`}
>
{props.outputs?.map((output, index) => (
<Output output={createImmutableOutput(output)} key={index}>
<TransformMedia
output_type={"display_data"}
id={props.id}
contentRef={props.contentRef}
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
/>
<TransformMedia
output_type={"execute_result"}
id={props.id}
contentRef={props.contentRef}
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
/>
<KernelOutputError />
<StreamText />
</Output>
))}
</div>
);
ReactDOM.render(outputs, document.getElementById("cellOutput"));
}
);
};
// Entry point
window.addEventListener("load", onInit);

View File

@@ -0,0 +1,138 @@
import { ImmutableDisplayData, ImmutableExecuteResult, JSONObject } from "@nteract/commutable";
// import outputs individually to avoid increasing the bundle size
import { HTML } from "@nteract/outputs/lib/components/media/html";
import { Image } from "@nteract/outputs/lib/components/media/image";
import { JavaScript } from "@nteract/outputs/lib/components/media/javascript";
import { Json } from "@nteract/outputs/lib/components/media/json";
import { LaTeX } from "@nteract/outputs/lib/components/media/latex";
import { Plain } from "@nteract/outputs/lib/components/media/plain";
import { SVG } from "@nteract/outputs/lib/components/media/svg";
import { ContentRef } from "@nteract/types";
import React, { Suspense } from "react";
const EmptyTransform = (): JSX.Element => <></>;
const displayOrder = [
"application/vnd.jupyter.widget-view+json",
"application/vnd.vega.v5+json",
"application/vnd.vega.v4+json",
"application/vnd.vega.v3+json",
"application/vnd.vega.v2+json",
"application/vnd.vegalite.v4+json",
"application/vnd.vegalite.v3+json",
"application/vnd.vegalite.v2+json",
"application/vnd.vegalite.v1+json",
"application/geo+json",
"application/vnd.plotly.v1+json",
"text/vnd.plotly.v1+html",
"application/x-nteract-model-debug+json",
"application/vnd.dataresource+json",
"application/vdom.v1+json",
"application/json",
"application/javascript",
"text/html",
"text/markdown",
"text/latex",
"image/svg+xml",
"image/gif",
"image/png",
"image/jpeg",
"text/plain",
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transformsById = new Map<string, React.ComponentType<any>>([
["text/vnd.plotly.v1+html", React.lazy(() => import("@nteract/transform-plotly"))],
["application/vnd.plotly.v1+json", React.lazy(() => import("@nteract/transform-plotly"))],
["application/geo+json", EmptyTransform], // TODO: The geojson transform will likely need some work because of the basemap URL(s)
["application/x-nteract-model-debug+json", React.lazy(() => import("@nteract/transform-model-debug"))],
["application/vnd.dataresource+json", React.lazy(() => import("@nteract/data-explorer"))],
["application/vnd.jupyter.widget-view+json", React.lazy(() => import("./transforms/WidgetDisplay"))],
["application/vnd.vegalite.v1+json", React.lazy(() => import("./transforms/VegaLite1"))],
["application/vnd.vegalite.v2+json", React.lazy(() => import("./transforms/VegaLite2"))],
["application/vnd.vegalite.v3+json", React.lazy(() => import("./transforms/VegaLite3"))],
["application/vnd.vegalite.v4+json", React.lazy(() => import("./transforms/VegaLite4"))],
["application/vnd.vega.v2+json", React.lazy(() => import("./transforms/Vega2"))],
["application/vnd.vega.v3+json", React.lazy(() => import("./transforms/Vega3"))],
["application/vnd.vega.v4+json", React.lazy(() => import("./transforms/Vega4"))],
["application/vnd.vega.v5+json", React.lazy(() => import("./transforms/Vega5"))],
["application/vdom.v1+json", React.lazy(() => import("@nteract/transform-vdom"))],
["application/json", Json],
["application/javascript", JavaScript],
["text/html", HTML],
["text/markdown", React.lazy(() => import("@nteract/outputs/lib/components/media/markdown"))], // Markdown increases the bundle size so lazy load it
["text/latex", LaTeX],
["image/svg+xml", SVG],
["image/gif", Image],
["image/png", Image],
["image/jpeg", Image],
["text/plain", Plain],
]);
interface TransformMediaProps {
output_type: string;
id: string;
contentRef: ContentRef;
output?: ImmutableDisplayData | ImmutableExecuteResult;
onMetadataChange: (metadata: JSONObject, mediaType: string) => void;
}
export const TransformMedia = (props: TransformMediaProps): JSX.Element => {
const { Media, mediaType, data, metadata } = getMediaInfo(props);
// If we had no valid result, return an empty output
if (!mediaType || !data) {
return <></>;
}
return (
<Suspense fallback={<div>Loading...</div>}>
<Media
onMetadataChange={props.onMetadataChange}
data={data}
metadata={metadata}
contentRef={props.contentRef}
id={props.id}
/>
</Suspense>
);
};
const getMediaInfo = (props: TransformMediaProps) => {
const { output, output_type } = props;
// This component should only be used with display data and execute result
if (!output || !(output_type === "display_data" || output_type === "execute_result")) {
console.warn("connected transform media managed to get a non media bundle output");
return {
Media: EmptyTransform,
};
}
// Find the first mediaType in the output data that we support with a handler
const mediaType = displayOrder.find(
(key) =>
Object.prototype.hasOwnProperty.call(output.data, key) &&
(Object.prototype.hasOwnProperty.call(transformsById, key) || transformsById.get(key))
);
if (mediaType) {
const metadata = output.metadata.get(mediaType);
const data = output.data[mediaType];
const Media = transformsById.get(mediaType);
return {
Media,
mediaType,
data,
metadata,
};
}
return {
Media: EmptyTransform,
mediaType,
output,
};
};
export default TransformMedia;

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Cell Output Viewer</title>
</head>
<body>
<div class="cellOutput" id="cellOutput"></div>
</body>
</html>

View File

@@ -0,0 +1 @@
export { Vega2 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { Vega3 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { Vega4 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { Vega5 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { VegaLite1 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { VegaLite2 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { VegaLite3 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { VegaLite4 as default } from "@nteract/transform-vega";

View File

@@ -0,0 +1 @@
export { WidgetDisplay as default } from "@nteract/jupyter-widgets";

View File

@@ -0,0 +1,61 @@
import { DatePicker, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent } from "react";
export interface TableEntityProps {
entityValueLabel?: string;
entityValuePlaceholder: string;
entityValue: string | Date;
isEntityTypeDate: boolean;
entityTimeValue: string;
entityValueType: string;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
}
export const EntityValue: FunctionComponent<TableEntityProps> = ({
entityValueLabel,
entityValuePlaceholder,
entityValue,
isEntityTypeDate,
entityTimeValue,
entityValueType,
onEntityValueChange,
onSelectDate,
onEntityTimeValueChange,
}: TableEntityProps): JSX.Element => {
if (isEntityTypeDate) {
return (
<>
<DatePicker
className="addEntityDatePicker"
placeholder={entityValuePlaceholder}
value={entityValue && new Date(entityValue)}
ariaLabel={entityValuePlaceholder}
onSelectDate={onSelectDate}
/>
<TextField
label={entityValueLabel && entityValueLabel}
id="entityTimeId"
autoFocus
type="time"
value={entityTimeValue}
onChange={onEntityTimeValueChange}
/>
</>
);
}
return (
<TextField
label={entityValueLabel && entityValueLabel}
className="addEntityTextField"
id="entityValueId"
autoFocus
type={entityValueType}
placeholder={entityValuePlaceholder}
value={typeof entityValue === "string" && entityValue}
onChange={onEntityValueChange}
/>
);
};

View File

@@ -5,11 +5,10 @@ import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -348,10 +347,7 @@ export function getEndpoint(): string {
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> { async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
const errorMessage = await response.text(); const errorMessage = await response.text();
// Log the error where the user can see it // Log the error where the user can see it
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`);
ConsoleDataType.Error,
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
);
if (response.status === HttpStatusCodes.Forbidden) { if (response.status === HttpStatusCodes.Forbidden) {
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
return; return;

136
src/Common/TableEntity.tsx Normal file
View File

@@ -0,0 +1,136 @@
import {
Dropdown,
IDropdownOption,
IDropdownStyles,
IImageProps,
Image,
IStackTokens,
Stack,
TextField,
TooltipHost,
} from "office-ui-fabric-react";
import React, { FunctionComponent } from "react";
import DeleteIcon from "../../images/delete.svg";
import EditIcon from "../../images/Edit_entity.svg";
import { CassandraType, TableType } from "../Explorer/Tables/Constants";
import { userContext } from "../UserContext";
import { EntityValue } from "./EntityValue";
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
export interface TableEntityProps {
entityTypeLabel?: string;
entityPropertyLabel?: string;
entityValueLabel?: string;
isDeleteOptionVisible: boolean;
entityProperty: string;
entityPropertyPlaceHolder: string;
selectedKey: string | number;
entityValuePlaceholder: string;
entityValue: string | Date;
isEntityTypeDate: boolean;
options: { key: string; text: string }[];
isPropertyTypeDisable: boolean;
entityTimeValue: string;
onDeleteEntity?: () => void;
onEditEntity?: () => void;
onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onEntityTypeChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption) => void;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
}
export const TableEntity: FunctionComponent<TableEntityProps> = ({
entityTypeLabel,
entityPropertyLabel,
isDeleteOptionVisible,
entityProperty,
selectedKey,
entityPropertyPlaceHolder,
entityValueLabel,
entityValuePlaceholder,
entityValue,
options,
isPropertyTypeDisable,
isEntityTypeDate,
entityTimeValue,
onEditEntity,
onDeleteEntity,
onEntityPropertyChange,
onEntityTypeChange,
onEntityValueChange,
onSelectDate,
onEntityTimeValueChange,
}: TableEntityProps): JSX.Element => {
const imageProps: IImageProps = {
width: 16,
height: 30,
className: entityPropertyLabel ? "addRemoveIconLabel" : "addRemoveIcon",
};
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
const getEntityValueType = (): string => {
const { Int, Smallint, Tinyint } = CassandraType;
const { Double, Int32, Int64 } = TableType;
if (
selectedKey === Double ||
selectedKey === Int32 ||
selectedKey === Int64 ||
selectedKey === Int ||
selectedKey === Smallint ||
selectedKey === Tinyint
) {
return "number";
}
return "string";
};
return (
<>
<Stack horizontal tokens={sectionStackTokens}>
<TextField
label={entityPropertyLabel && entityPropertyLabel}
id="entityPropertyId"
autoFocus
disabled={isPropertyTypeDisable}
placeholder={entityPropertyPlaceHolder}
value={entityProperty}
onChange={onEntityPropertyChange}
required
/>
<Dropdown
label={entityTypeLabel && entityTypeLabel}
selectedKey={selectedKey}
onChange={onEntityTypeChange}
options={options}
disabled={isPropertyTypeDisable}
id="entityTypeId"
styles={dropdownStyles}
/>
<EntityValue
entityValueLabel={entityValueLabel}
entityValueType={getEntityValueType()}
entityValuePlaceholder={entityValuePlaceholder}
entityValue={entityValue}
isEntityTypeDate={isEntityTypeDate}
entityTimeValue={entityTimeValue}
onEntityValueChange={onEntityValueChange}
onSelectDate={onSelectDate}
onEntityTimeValueChange={onEntityTimeValueChange}
/>
<TooltipHost content="Edit property" id="editTooltip">
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
</TooltipHost>
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
<TooltipHost content="Delete property" id="deleteTooltip">
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} />
</TooltipHost>
)}
</Stack>
</>
);
};

View File

@@ -1,8 +1,8 @@
import { Image, Stack, TextField } from "office-ui-fabric-react"; import { Image, Stack, TextField } from "office-ui-fabric-react";
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react"; import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
import FolderIcon from "../../../images/folder_16x16.svg"; import FolderIcon from "../../../images/folder_16x16.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../Constants";
import { Tooltip } from "../Tooltip"; import { Tooltip } from "../Tooltip/Tooltip";
interface UploadProps { interface UploadProps {
label: string; label: string;

View File

@@ -22,9 +22,12 @@ export const bulkCreateDocument = async (
); );
const successCount = response.filter((r) => r.statusCode === 201).length; const successCount = response.filter((r) => r.statusCode === 201).length;
const throttledCount = response.filter((r) => r.statusCode === 429).length;
logConsoleInfo( logConsoleInfo(
`${documents.length} operations completed for container ${collection.id()}. ${successCount} operations succeeded` `${
documents.length
} operations completed for container ${collection.id()}. ${successCount} operations succeeded. ${throttledCount} operations throttled`
); );
return response; return response;
} catch (error) { } catch (error) {

View File

@@ -26,6 +26,7 @@ export interface ConfigContext {
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
hostedExplorerURL: string; hostedExplorerURL: string;
armAPIVersion?: string; armAPIVersion?: string;
allowedJunoOrigins: string[];
} }
// Default configuration // Default configuration
@@ -53,6 +54,13 @@ let configContext: Readonly<ConfigContext> = {
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
JUNO_ENDPOINT: "https://tools.cosmos.azure.com", JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
allowedJunoOrigins: [
"https://juno-test.documents-dev.windows-int.net",
"https://juno-test2.documents-dev.windows-int.net",
"https://tools.cosmos.azure.com",
"https://tools-staging.cosmos.azure.com",
"https://localhost",
],
}; };
export function resetConfigContext(): void { export function resetConfigContext(): void {
@@ -86,13 +94,18 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
}); });
if (response.status === 200) { if (response.status === 200) {
try { try {
const { allowedParentFrameOrigins, ...externalConfig } = await response.json(); const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig); Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) { if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({ updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins], allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
}); });
} }
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
updateConfigContext({
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
});
}
} catch (error) { } catch (error) {
console.error("Unable to parse json in config file"); console.error("Unable to parse json in config file");
console.error(error); console.error(error);

View File

@@ -4,6 +4,7 @@
export enum TabKind { export enum TabKind {
SQLDocuments, SQLDocuments,
MongoDocuments, MongoDocuments,
SchemaAnalyzer,
TableEntities, TableEntities,
Graph, Graph,
SQLQuery, SQLQuery,

View File

@@ -141,6 +141,7 @@ export interface Collection extends CollectionBase {
onTableEntitiesClick(): void; onTableEntitiesClick(): void;
onGraphDocumentsClick(): void; onGraphDocumentsClick(): void;
onMongoDBDocumentsClick(): void; onMongoDBDocumentsClick(): void;
onSchemaAnalyzerClick(): void;
openTab(): void; openTab(): void;
onSettingsClick: () => Promise<void>; onSettingsClick: () => Promise<void>;
@@ -276,7 +277,6 @@ export interface TabOptions {
tabKind: CollectionTabKind; tabKind: CollectionTabKind;
title: string; title: string;
tabPath: string; tabPath: string;
isActive: ko.Observable<boolean>;
hashLocation: string; hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void; onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>; isTabsContentExpanded?: ko.Observable<boolean>;
@@ -367,6 +367,7 @@ export enum CollectionTabKind {
Schema = 19, Schema = 19,
CollectionSettingsV2 = 20, CollectionSettingsV2 = 20,
DatabaseSettingsV2 = 21, DatabaseSettingsV2 = 21,
SchemaAnalyzer = 22,
} }
export enum TerminalKind { export enum TerminalKind {

View File

@@ -8,10 +8,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("input-typeahead")).toBe(true); expect(ko.components.isRegistered("input-typeahead")).toBe(true);
}); });
it("should register new-vertex-form component", () => {
expect(ko.components.isRegistered("new-vertex-form")).toBe(true);
});
it("should register error-display component", () => { it("should register error-display component", () => {
expect(ko.components.isRegistered("error-display")).toBe(true); expect(ko.components.isRegistered("error-display")).toBe(true);
}); });
@@ -73,10 +69,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true); expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
}); });
it("should register graph-new-vertex-pane component", () => {
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
});
it("should register graph-styling-pane component", () => { it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true); expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
}); });
@@ -85,10 +77,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true); expect(ko.components.isRegistered("string-input-pane")).toBe(true);
}); });
it("should register setup-notebooks-pane component", () => {
expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true);
});
it("should register dynamic-list component", () => { it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true); expect(ko.components.isRegistered("dynamic-list")).toBe(true);
}); });

View File

@@ -7,7 +7,6 @@ import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahea
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent"; import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import * as PaneComponents from "./Panes/PaneComponents"; import * as PaneComponents from "./Panes/PaneComponents";
import ConflictsTab from "./Tabs/ConflictsTab"; import ConflictsTab from "./Tabs/ConflictsTab";
import DocumentsTab from "./Tabs/DocumentsTab"; import DocumentsTab from "./Tabs/DocumentsTab";
@@ -18,15 +17,14 @@ import NotebookTabV2 from "./Tabs/NotebookV2Tab";
import NotebookViewerTab from "./Tabs/NotebookViewerTab"; import NotebookViewerTab from "./Tabs/NotebookViewerTab";
import QueryTab from "./Tabs/QueryTab"; import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab"; import QueryTablesTab from "./Tabs/QueryTablesTab";
import SchemaAnalyzerTab from "./Tabs/SchemaAnalyzerTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2"; import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab"; import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TabsManagerTemplate from "./Tabs/TabsManager.html";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import TriggerTab from "./Tabs/TriggerTab"; import TriggerTab from "./Tabs/TriggerTab";
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab"; 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("error-display", new ErrorDisplayComponent()); ko.components.register("error-display", new ErrorDisplayComponent());
ko.components.register("graph-style", GraphStyleComponent); ko.components.register("graph-style", GraphStyleComponent);
ko.components.register("editor", new EditorComponent()); ko.components.register("editor", new EditorComponent());
@@ -34,7 +32,6 @@ 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 });
// Collection Tabs // Collection Tabs
[ [
@@ -53,17 +50,15 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
GalleryTab, GalleryTab,
NotebookViewerTab, NotebookViewerTab,
DatabaseSettingsTabV2, DatabaseSettingsTabV2,
SchemaAnalyzerTab,
].forEach(({ component: { name, template } }) => ko.components.register(name, { template })); ].forEach(({ component: { name, template } }) => ko.components.register(name, { template }));
// Panes // Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent()); ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

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

View File

@@ -1,8 +1,3 @@
import * as React from "react";
import { Dialog as FluentDialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { import {
ChoiceGroup, ChoiceGroup,
FontIcon, FontIcon,
@@ -10,6 +5,11 @@ import {
IProgressIndicatorProps, IProgressIndicatorProps,
ProgressIndicator, ProgressIndicator,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { DefaultButton, IButtonProps, PrimaryButton } from "office-ui-fabric-react/lib/Button";
import { Dialog as FluentDialog, DialogFooter, DialogType, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
import { Link } from "office-ui-fabric-react/lib/Link";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import React, { FunctionComponent } from "react";
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {
label: string; label: string;
@@ -50,61 +50,69 @@ const DIALOG_TITLE_FONT_SIZE = "17px";
const DIALOG_TITLE_FONT_WEIGHT = 400; const DIALOG_TITLE_FONT_WEIGHT = 400;
const DIALOG_SUBTEXT_FONT_SIZE = "15px"; const DIALOG_SUBTEXT_FONT_SIZE = "15px";
export class Dialog extends React.Component<DialogProps> { export const Dialog: FunctionComponent<DialogProps> = ({
constructor(props: DialogProps) { title,
super(props); subText,
} isModal,
visible,
public render(): JSX.Element { choiceGroupProps,
const dialogProps: IDialogProps = { textFieldProps,
hidden: !this.props.visible, linkProps,
dialogContentProps: { progressIndicatorProps,
type: this.props.type || DialogType.normal, primaryButtonText,
title: this.props.title, secondaryButtonText,
subText: this.props.subText, onPrimaryButtonClick,
styles: { onSecondaryButtonClick,
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT }, primaryButtonDisabled,
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }, type,
}, showCloseButton,
showCloseButton: this.props.showCloseButton || false, onDismiss,
onDismiss: this.props.onDismiss, }: DialogProps) => {
const dialogProps: IDialogProps = {
hidden: !visible,
dialogContentProps: {
type: type || DialogType.normal,
title,
subText,
styles: {
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE },
}, },
modalProps: { isBlocking: this.props.isModal, isDarkOverlay: false }, showCloseButton: showCloseButton || false,
minWidth: DIALOG_MIN_WIDTH, onDismiss,
maxWidth: DIALOG_MAX_WIDTH, },
}; modalProps: { isBlocking: isModal, isDarkOverlay: false },
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps; minWidth: DIALOG_MIN_WIDTH,
const textFieldProps: ITextFieldProps = this.props.textFieldProps; maxWidth: DIALOG_MAX_WIDTH,
const linkProps: LinkProps = this.props.linkProps; };
const progressIndicatorProps: IProgressIndicatorProps = this.props.progressIndicatorProps;
const primaryButtonProps: IButtonProps = {
text: this.props.primaryButtonText,
disabled: this.props.primaryButtonDisabled || false,
onClick: this.props.onPrimaryButtonClick,
};
const secondaryButtonProps: IButtonProps =
this.props.secondaryButtonText && this.props.onSecondaryButtonClick
? {
text: this.props.secondaryButtonText,
onClick: this.props.onSecondaryButtonClick,
}
: undefined;
return ( const primaryButtonProps: IButtonProps = {
<FluentDialog {...dialogProps}> text: primaryButtonText,
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />} disabled: primaryButtonDisabled || false,
{textFieldProps && <TextField {...textFieldProps} />} onClick: onPrimaryButtonClick,
{linkProps && ( };
<Link href={linkProps.linkUrl} target="_blank"> const secondaryButtonProps: IButtonProps =
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" /> secondaryButtonText && onSecondaryButtonClick
</Link> ? {
)} text: secondaryButtonText,
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />} onClick: onSecondaryButtonClick,
<DialogFooter> }
<PrimaryButton {...primaryButtonProps} /> : undefined;
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
</DialogFooter> return (
</FluentDialog> <FluentDialog {...dialogProps}>
); {choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
} {textFieldProps && <TextField {...textFieldProps} />}
} {linkProps && (
<Link href={linkProps.linkUrl} target="_blank">
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
</DialogFooter>
</FluentDialog>
);
};

View File

@@ -5,6 +5,7 @@ import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent"; import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
@@ -38,7 +39,6 @@ describe("SettingsComponent", () => {
tabPath: "", tabPath: "",
node: undefined, node: undefined,
hashLocation: "settings", hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
}), }),
}; };
@@ -107,7 +107,13 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false); expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
const newContainer = new Explorer(); const newContainer = new Explorer();
newContainer.isPreferredApiCassandra = ko.computed(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DataModels.DatabaseAccount,
});
const newCollection = { ...collection }; const newCollection = { ...collection };
newCollection.container = newContainer; newCollection.container = newContainer;

View File

@@ -137,7 +137,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.offer = this.collection?.offer(); this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl(); this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor = this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB(); this.container && userContext.apiType !== "Cassandra" && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -299,7 +299,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected; this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean => public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection); this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean => public hasConflictResolution = (): boolean =>
this.container?.databaseAccount && this.container?.databaseAccount &&

View File

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

View File

@@ -320,8 +320,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public getPartitionKeyVisible = (): boolean => { public getPartitionKeyVisible = (): boolean => {
if ( if (
this.props.container.isPreferredApiCassandra() || userContext.apiType === "Cassandra" ||
this.props.container.isPreferredApiTable() || userContext.apiType === "Tables" ||
!this.props.collection.partitionKeyProperty || !this.props.collection.partitionKeyProperty ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey) (this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
) { ) {

View File

@@ -177,42 +177,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
EditTableEntityPane { EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -247,22 +211,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
@@ -324,20 +272,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
], ],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
@@ -467,42 +401,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -638,7 +536,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
@@ -647,10 +544,7 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -661,22 +555,6 @@ exports[`SettingsComponent renders 1`] = `
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -724,20 +602,6 @@ exports[`SettingsComponent renders 1`] = `
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,
"setupNotebooksPane": SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splitter": Splitter { "splitter": Splitter {
@@ -941,42 +805,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
EditTableEntityPane { EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -1011,22 +839,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
@@ -1088,20 +900,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
], ],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
@@ -1231,42 +1029,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -1402,7 +1164,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
@@ -1411,10 +1172,7 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -1425,22 +1183,6 @@ exports[`SettingsComponent renders 1`] = `
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -1488,20 +1230,6 @@ exports[`SettingsComponent renders 1`] = `
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,
"setupNotebooksPane": SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splitter": Splitter { "splitter": Splitter {
@@ -1718,42 +1446,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
EditTableEntityPane { EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -1788,22 +1480,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
@@ -1865,20 +1541,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
], ],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
@@ -2008,42 +1670,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -2179,7 +1805,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
@@ -2188,10 +1813,7 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -2202,22 +1824,6 @@ exports[`SettingsComponent renders 1`] = `
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -2265,20 +1871,6 @@ exports[`SettingsComponent renders 1`] = `
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,
"setupNotebooksPane": SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splitter": Splitter { "splitter": Splitter {
@@ -2482,42 +2074,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
EditTableEntityPane { EditTableEntityPane {
"addButtonLabel": "Add Property", "addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name", "attributeNameLabel": "Property Name",
@@ -2552,22 +2108,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
@@ -2629,20 +2169,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
], ],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
@@ -2772,42 +2298,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function], "addDatabaseText": [Function],
"addTableEntityPane": AddTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"enterRequiredValueLabel": "Enter identifier value.",
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "addtableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -2943,7 +2433,6 @@ exports[`SettingsComponent renders 1`] = `
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
@@ -2952,10 +2441,7 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -2966,22 +2452,6 @@ exports[`SettingsComponent renders 1`] = `
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane {
"buildString": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "newvertexpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onMoreDetailsKeyPress": [Function],
"onSubmitCreateCallback": null,
"partitionKeyProperty": [Function],
"tempVertexData": [Function],
"title": [Function],
"visible": [Function],
},
"notebookBasePath": [Function], "notebookBasePath": [Function],
"notebookServerInfo": [Function], "notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -3029,20 +2499,6 @@ exports[`SettingsComponent renders 1`] = `
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
"setNotificationConsoleData": undefined, "setNotificationConsoleData": undefined,
"setupNotebooksPane": SetupNotebooksPane {
"container": [Circular],
"description": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "setupnotebookspane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onCompleteSetupClick": [Function],
"onCompleteSetupKeyPress": [Function],
"title": [Function],
"visible": [Function],
},
"signInAad": [Function], "signInAad": [Function],
"sparkClusterConnectionInfo": [Function], "sparkClusterConnectionInfo": [Function],
"splitter": Splitter { "splitter": Splitter {

View File

@@ -14,10 +14,7 @@ describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => { const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer; const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]); explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false);
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false); explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
explorerStub.findDatabaseWithId = () => database; explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve(); explorerStub.refreshAllDatabases = () => Q.resolve();
@@ -116,7 +113,13 @@ describe("ContainerSampleGenerator", () => {
collection.databaseId = database.id(); collection.databaseId = database.id();
const explorerStub = createExplorerStub(database); const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableGremlin" }],
},
} as DatabaseAccount,
});
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
@@ -156,8 +159,6 @@ describe("ContainerSampleGenerator", () => {
it("should not create any sample for Cassandra API account", async () => { it("should not create any sample for Cassandra API account", async () => {
const experience = "Sample generation not supported for this API Cassandra"; const experience = "Sample generation not supported for this API Cassandra";
const explorerStub = createExplorerStub(undefined);
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
properties: { properties: {
@@ -165,6 +166,7 @@ describe("ContainerSampleGenerator", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
const explorerStub = createExplorerStub(undefined);
// Rejects with error that contains experience // Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });

View File

@@ -73,7 +73,7 @@ export class ContainerSampleGenerator {
} }
const promises: Q.Promise<any>[] = []; const promises: Q.Promise<any>[] = [];
if (this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
// For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries // For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries
// (e.g. adding edge requires vertices to be present) // (e.g. adding edge requires vertices to be present)
const queries: string[] = this.sampleDataFile.data; const queries: string[] = this.sampleDataFile.data;

View File

@@ -1,8 +1,7 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil { export class DataSamplesUtil {
@@ -21,18 +20,16 @@ export class DataSamplesUtil {
if (this.hasContainer(databaseName, containerName, this.container.databases())) { if (this.hasContainer(databaseName, containerName, this.container.databases())) {
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); logConsoleError(msg);
return; return;
} }
await generator await generator
.createSampleContainerAsync() .createSampleContainerAsync()
.catch((error) => .catch((error) => logConsoleError(`Error creating sample container: ${error}`));
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Error creating sample container: ${error}`)
);
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg); logConsoleInfo(msg);
} }
/** /**
@@ -57,6 +54,6 @@ export class DataSamplesUtil {
} }
public isSampleContainerCreationSupported(): boolean { public isSampleContainerCreationSupported(): boolean {
return userContext.apiType === "SQL" || this.container.isPreferredApiGraph(); return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
} }
} }

View File

@@ -36,6 +36,7 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import * as PricingUtils from "../Utils/PricingUtils"; import * as PricingUtils from "../Utils/PricingUtils";
import * as ComponentRegisterer from "./ComponentRegisterer"; import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
@@ -43,34 +44,35 @@ import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandBut
import { DialogProps, TextFieldProps } from "./Controls/Dialog"; 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 } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import * as 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 { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPanel } from "./Panes/LoadQueryPanel"; import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SaveQueryPanel } from "./Panes/SaveQueryPanel"; import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
import { SettingsPane } from "./Panes/SettingsPane"; import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel"; import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import TabsBase from "./Tabs/TabsBase"; import TabsBase from "./Tabs/TabsBase";
import { TabsManager } from "./Tabs/TabsManager"; import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
@@ -116,26 +118,11 @@ export default class Explorer {
* Use userContext.apiType instead * Use userContext.apiType instead
* */ * */
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra"
* */
public isPreferredApiCassandra: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo" * Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */ * */
public isPreferredApiMongoDB: ko.Computed<boolean>; public isPreferredApiMongoDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
* */
public isPreferredApiGraph: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
* */
public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
@@ -186,20 +173,15 @@ export default class Explorer {
public addDatabasePane: AddDatabasePane; public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane; public addCollectionPane: AddCollectionPane;
public graphStylingPane: GraphStylingPane; public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane; public editTableEntityPane: EditTableEntityPane;
public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public stringInputPane: StringInputPane; public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: ReactAdapter; public publishNotebookPaneAdapter: ReactAdapter;
public copyNotebookPaneAdapter: ReactAdapter;
// features // features
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>; public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>; public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>; public isRightPanelV2Enabled: ko.Computed<boolean>;
public isMongoIndexingEnabled: ko.Observable<boolean>; public isMongoIndexingEnabled: ko.Observable<boolean>;
@@ -297,7 +279,6 @@ export default class Explorer {
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) || ((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
userContext.features.enableNotebooks) userContext.features.enableNotebooks)
); );
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled: this.isNotebookEnabled(), isNotebookEnabled: this.isNotebookEnabled(),
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
@@ -350,7 +331,6 @@ export default class Explorer {
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.canExceedMaximumValue = ko.computed<boolean>(() => userContext.features.canExceedMaximumValue); this.canExceedMaximumValue = ko.computed<boolean>(() => userContext.features.canExceedMaximumValue);
@@ -414,20 +394,6 @@ export default class Explorer {
}); });
}); });
this.isPreferredApiCassandra = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase();
});
this.isPreferredApiGraph = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase();
});
this.isPreferredApiTable = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase();
});
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) { if (userContext.features.enableFixedCollectionWithSharedThroughput) {
return true; return true;
@@ -486,7 +452,9 @@ export default class Explorer {
this.isHostedDataExplorerEnabled = ko.computed<boolean>( this.isHostedDataExplorerEnabled = ko.computed<boolean>(
() => () =>
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() configContext.platform === Platform.Portal &&
!this.isRunningOnNationalCloud() &&
userContext.apiType !== "Gremlin"
); );
this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2); this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2);
this.selectedDatabaseId = ko.computed<string>(() => { this.selectedDatabaseId = ko.computed<string>(() => {
@@ -518,7 +486,7 @@ export default class Explorer {
}); });
this.addCollectionPane = new AddCollectionPane({ this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), isPreferredApiTable: ko.computed(() => userContext.apiType === "Tables"),
id: "addcollectionpane", id: "addcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -532,13 +500,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.addTableEntityPane = new AddTableEntityPane({
id: "addtableentitypane",
visible: ko.observable<boolean>(false),
container: this,
});
this.editTableEntityPane = new EditTableEntityPane({ this.editTableEntityPane = new EditTableEntityPane({
id: "edittableentitypane", id: "edittableentitypane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -546,13 +507,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.newVertexPane = new NewVertexPane({
id: "newvertexpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane", id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -567,25 +521,21 @@ export default class Explorer {
container: this, container: this,
}); });
this.setupNotebooksPane = new SetupNotebooksPane({
id: "setupnotebookspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.tabsManager = params?.tabsManager ?? new TabsManager(); this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) {
this.selectedNode(undefined);
this.onUpdateTabsButtons([]);
}
});
this._panes = [ this._panes = [
this.addDatabasePane, this.addDatabasePane,
this.addCollectionPane, this.addCollectionPane,
this.graphStylingPane, this.graphStylingPane,
this.addTableEntityPane,
this.editTableEntityPane, this.editTableEntityPane,
this.newVertexPane,
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.stringInputPane, this.stringInputPane,
this.setupNotebooksPane,
]; ];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
@@ -654,7 +604,6 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id"); this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Entity");
this.editTableEntityPane.title("Edit Table Entity"); this.editTableEntityPane.title("Edit Table Entity");
this.tableDataClient = new TablesAPIDataClient(); this.tableDataClient = new TablesAPIDataClient();
break; break;
@@ -669,7 +618,6 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id"); this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.addTableEntityPane.title("Add Table Row");
this.editTableEntityPane.title("Edit Table Row"); this.editTableEntityPane.title("Edit Table Row");
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
@@ -765,8 +713,7 @@ export default class Explorer {
onPrimaryButtonClick: async () => { onPrimaryButtonClick: async () => {
const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
const logId = NotificationConsoleUtils.logConsoleMessage( const clearInProgressMessage = logConsoleProgress(
ConsoleDataType.InProgress,
"Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account." "Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account."
); );
this.isSynapseLinkUpdating(true); this.isSynapseLinkUpdating(true);
@@ -784,19 +731,13 @@ export default class Explorer {
}, },
} }
); );
NotificationConsoleUtils.clearInProgressMessageWithId(logId); clearInProgressMessage();
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo("Enabled Azure Synapse Link for this account");
ConsoleDataType.Info,
"Enabled Azure Synapse Link for this account"
);
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime); TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
} catch (error) { } catch (error) {
NotificationConsoleUtils.clearInProgressMessageWithId(logId); clearInProgressMessage();
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
ConsoleDataType.Error,
`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`
);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime); TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
} finally { } finally {
this.isSynapseLinkUpdating(false); this.isSynapseLinkUpdating(false);
@@ -923,10 +864,7 @@ export default class Explorer {
}, },
startKey startKey
); );
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error while refreshing databases: ${errorMessage}`);
ConsoleDataType.Error,
`Error while refreshing databases: ${errorMessage}`
);
} }
); );
@@ -1147,20 +1085,20 @@ export default class Explorer {
private _resetNotebookWorkspace = async () => { private _resetNotebookWorkspace = async () => {
this._closeModalDialog(); this._closeModalDialog();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace"); const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
try { try {
await this.notebookManager?.notebookClient.resetWorkspace(); await this.notebookManager?.notebookClient.resetWorkspace();
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace"); logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
} catch (error) { } catch (error) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`); logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
error: getErrorMessage(error), error: getErrorMessage(error),
errorStack: getErrorStack(error), errorStack: getErrorStack(error),
}); });
throw error; throw error;
} finally { } finally {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearInProgressMessage();
} }
}; };
@@ -1495,11 +1433,7 @@ export default class Explorer {
} }
public copyNotebook(name: string, content: string): void { public copyNotebook(name: string, content: string): void {
if (this.notebookManager) { this.notebookManager?.openCopyNotebookPane(name, content);
this.notebookManager.openCopyNotebookPane(name, content);
this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter;
this.isCopyNotebookPaneEnabled(true);
}
} }
public showOkModalDialog(title: string, msg: string): void { public showOkModalDialog(title: string, msg: string): void {
@@ -1586,7 +1520,6 @@ export default class Explorer {
collection: null, collection: null,
masterKey: userContext.masterKey || "", masterKey: userContext.masterKey || "",
hashLocation: "notebooks", hashLocation: "notebooks",
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null, onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons, onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -1718,11 +1651,7 @@ export default class Explorer {
clearMessage(); clearMessage();
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
ConsoleDataType.Error,
`Could not download notebook ${getErrorMessage(error)}`
);
clearMessage(); clearMessage();
} }
); );
@@ -1874,15 +1803,8 @@ export default class Explorer {
} }
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
() => { () => logConsoleInfo(`Successfully deleted: ${item.path}`),
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`); (reason: any) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
},
(reason: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete "${item.path}": ${JSON.stringify(reason)}`
);
}
); );
} }
@@ -1898,11 +1820,7 @@ export default class Explorer {
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.resourceTree.myNotebooksContentRoot;
const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
ConsoleDataType.InProgress,
`Creating new notebook in ${parent.path}`
);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
@@ -1910,7 +1828,7 @@ export default class Explorer {
this.notebookManager?.notebookContentClient this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent) .createNewNotebookFile(parent)
.then((newFile: NotebookContentItem) => { .then((newFile: NotebookContentItem) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`); logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.CreateNewNotebook, Action.CreateNewNotebook,
{ {
@@ -1923,7 +1841,7 @@ export default class Explorer {
.then(() => this.resourceTree.triggerRender()) .then(() => this.resourceTree.triggerRender())
.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); logConsoleError(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateNewNotebook, Action.CreateNewNotebook,
{ {
@@ -1934,7 +1852,7 @@ export default class Explorer {
startKey startKey
); );
}) })
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId)); .finally(clearInProgressMessage);
} }
public refreshContentItem(item: NotebookContentItem): Promise<void> { public refreshContentItem(item: NotebookContentItem): Promise<void> {
@@ -1992,7 +1910,6 @@ export default class Explorer {
tabPath: title, tabPath: title,
collection: null, collection: null,
hashLocation: hashLocation, hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null, onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons, onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2097,7 +2014,7 @@ export default class Explorer {
} }
public onNewCollectionClicked(): void { public onNewCollectionClicked(): void {
if (this.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
this.cassandraAddCollectionPane.open(); this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableReactPane) { } else if (userContext.features.enableReactPane) {
this.openAddCollectionPanel(); this.openAddCollectionPanel();
@@ -2145,7 +2062,7 @@ export default class Explorer {
const description = const description =
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account."; "You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
this.setupNotebooksPane.openWithTitleAndDescription(title, description); this.openSetupNotebooksPanel(title, description);
} }
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
@@ -2210,7 +2127,7 @@ export default class Explorer {
let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience); let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience);
this.openSidePanel( this.openSidePanel(
"Delete " + collectionName, "Delete " + collectionName,
<DeleteCollectionConfirmationPanel <DeleteCollectionConfirmationPane
explorer={this} explorer={this}
collectionName={collectionName} collectionName={collectionName}
closePanel={this.closeSidePanel} closePanel={this.closeSidePanel}
@@ -2241,7 +2158,7 @@ export default class Explorer {
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
this.openSidePanel( this.openSidePanel(
"Input parameters", "Input parameters",
<ExecuteSprocParamsPanel <ExecuteSprocParamsPane
explorer={this} explorer={this}
storedProcedure={storedProcedure} storedProcedure={storedProcedure}
closePanel={() => this.closeSidePanel()} closePanel={() => this.closeSidePanel()}
@@ -2262,15 +2179,15 @@ export default class Explorer {
} }
public openBrowseQueriesPanel(): void { public openBrowseQueriesPanel(): void {
this.openSidePanel("Open Saved Queries", <BrowseQueriesPanel explorer={this} closePanel={this.closeSidePanel} />); this.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} closePanel={this.closeSidePanel} />);
} }
public openLoadQueryPanel(): void { public openLoadQueryPanel(): void {
this.openSidePanel("Load Query", <LoadQueryPanel explorer={this} closePanel={() => this.closeSidePanel()} />); this.openSidePanel("Load Query", <LoadQueryPane explorer={this} closePanel={() => this.closeSidePanel()} />);
} }
public openSaveQueryPanel(): void { public openSaveQueryPanel(): void {
this.openSidePanel("Save Query", <SaveQueryPanel explorer={this} closePanel={() => this.closeSidePanel()} />); this.openSidePanel("Save Query", <SaveQueryPane explorer={this} closePanel={() => this.closeSidePanel()} />);
} }
public openUploadFilePanel(parent?: NotebookContentItem): void { public openUploadFilePanel(parent?: NotebookContentItem): void {
@@ -2285,6 +2202,31 @@ export default class Explorer {
); );
} }
public openAddTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
this.openSidePanel(
"Add Table Entity",
<AddTableEntityPanel
explorer={this}
closePanel={this.closeSidePanel}
queryTablesTab={queryTablesTab}
tableEntityListViewModel={tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public openSetupNotebooksPanel(title: string, description: string): void {
this.openSidePanel(
title,
<SetupNoteBooksPanel
explorer={this}
closePanel={this.closeSidePanel}
openNotificationConsole={() => this.expandConsole()}
panelTitle={title}
panelDescription={description}
/>
);
}
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void { public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
this.openSidePanel( this.openSidePanel(
"Select Column", "Select Column",

View File

@@ -1,24 +1,22 @@
import * as ko from "knockout"; import { BaseType } from "d3";
import Q from "q";
import { schemeCategory10 } from "d3-scale-chromatic";
import { selectAll, select } from "d3-selection";
import { zoom, zoomIdentity } from "d3-zoom";
import { scaleOrdinal } from "d3-scale";
import { forceSimulation, forceLink, forceCollide, forceManyBody } from "d3-force";
import { interpolateNumber, interpolate } from "d3-interpolate";
import { map as d3Map } from "d3-collection"; import { map as d3Map } from "d3-collection";
import { drag, D3DragEvent } from "d3-drag"; import { D3DragEvent, drag } from "d3-drag";
import { forceCollide, forceLink, forceManyBody, forceSimulation } from "d3-force";
import { interpolate, interpolateNumber } from "d3-interpolate";
import { scaleOrdinal } from "d3-scale";
import { schemeCategory10 } from "d3-scale-chromatic";
import { select, selectAll } from "d3-selection";
import { zoom, zoomIdentity } from "d3-zoom";
import * as ko from "knockout";
import Q from "q";
import _ from "underscore"; import _ from "underscore";
import { NeighborType } from "../../../Contracts/ViewModels";
import { GraphData, D3Node, D3Link } from "./GraphData";
import { HashMap } from "../../../Common/HashMap";
import { BaseType } from "d3";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab";
import { GraphExplorer } from "./GraphExplorer";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { HashMap } from "../../../Common/HashMap";
import { NeighborType } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "./GraphData";
import { GraphExplorer } from "./GraphExplorer";
export interface D3GraphIconMap { export interface D3GraphIconMap {
[key: string]: { data: string; format: string }; [key: string]: { data: string; format: string };
@@ -1005,7 +1003,7 @@ export class D3ForceGraph implements GraphRenderer {
*/ */
private loadNeighbors(v: D3Node, pageAction: PAGE_ACTION) { private loadNeighbors(v: D3Node, pageAction: PAGE_ACTION) {
if (!this.graphDataWrapper.hasVertexId(v.id)) { if (!this.graphDataWrapper.hasVertexId(v.id)) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Clicked node not in graph data. id: ${v.id}`); logConsoleError(`Clicked node not in graph data. id: ${v.id}`);
return; return;
} }

View File

@@ -1,37 +1,36 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as Q from "q"; import * as Q from "q";
import * as React from "react"; import * as React from "react";
import * as LeftPane from "./LeftPaneComponent";
import { MiddlePaneComponent } from "./MiddlePaneComponent";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as NodeProperties from "./NodePropertiesComponent";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GraphData from "./GraphData";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import * as GraphUtil from "./GraphUtil";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as GremlinClient from "./GremlinClient";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { ArraysByKeyCache } from "./ArraysByKeyCache";
import { EdgeInfoCache } from "./EdgeInfoCache";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import { QueryContainerComponent } from "./QueryContainerComponent";
import { GraphConfig } from "../../Tabs/GraphTab";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import LoadGraphIcon from "../../../../images/LoadGraph.png"; import LoadGraphIcon from "../../../../images/LoadGraph.png";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import * as Constants from "../../../Common/Constants";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import { FeedOptions } from "@azure/cosmos"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { InputProperty } from "../../../Contracts/ViewModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { GraphConfig } from "../../Tabs/GraphTab";
import { ArraysByKeyCache } from "./ArraysByKeyCache";
import * as D3ForceGraph from "./D3ForceGraph";
import { EdgeInfoCache } from "./EdgeInfoCache";
import * as GraphData from "./GraphData";
import * as GraphUtil from "./GraphUtil";
import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GremlinClient from "./GremlinClient";
import * as LeftPane from "./LeftPaneComponent";
import { MiddlePaneComponent } from "./MiddlePaneComponent";
import * as NodeProperties from "./NodePropertiesComponent";
import { QueryContainerComponent } from "./QueryContainerComponent";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
@@ -697,13 +696,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param cmd * @param cmd
*/ */
public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> { public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> {
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`); const clearConsoleProgress = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`);
this.setExecuteCounter(this.executeCounter + 1); this.setExecuteCounter(this.executeCounter + 1);
return this.gremlinClient.execute(cmd).then( return this.gremlinClient.execute(cmd).then(
(result: GremlinClient.GremlinRequestResult) => { (result: GremlinClient.GremlinRequestResult) => {
this.setExecuteCounter(this.executeCounter - 1); this.setExecuteCounter(this.executeCounter - 1);
GraphExplorer.clearConsoleProgress(id); clearConsoleProgress();
if (result.isIncomplete) { if (result.isIncomplete) {
const msg = `The query results are too large and only partial results are displayed for: ${cmd}`; const msg = `The query results are too large and only partial results are displayed for: ${cmd}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, msg); GraphExplorer.reportToConsole(ConsoleDataType.Error, msg);
@@ -718,7 +717,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
(err: string) => { (err: string) => {
this.setExecuteCounter(this.executeCounter - 1); this.setExecuteCounter(this.executeCounter - 1);
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Gremlin query failed: ${cmd}`, err); GraphExplorer.reportToConsole(ConsoleDataType.Error, `Gremlin query failed: ${cmd}`, err);
GraphExplorer.clearConsoleProgress(id); clearConsoleProgress();
throw err; throw err;
} }
); );
@@ -1083,13 +1082,26 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param errorData additional errors * @param errorData additional errors
* @return id * @return id
*/ */
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): string { public static reportToConsole(type: ConsoleDataType.InProgress, msg: string, ...errorData: any[]): () => void;
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr: string = ""; let errorDataStr: string = "";
if (errorData && errorData.length > 0) { if (errorData && errorData.length > 0) {
console.error(msg, errorData); console.error(msg, errorData);
errorDataStr = ": " + JSON.stringify(errorData); errorDataStr = ": " + JSON.stringify(errorData);
} }
return NotificationConsoleUtils.logConsoleMessage(type, `${msg}${errorDataStr}`);
const consoleMessage = `${msg}${errorDataStr}`;
switch (type) {
case ConsoleDataType.Error:
return logConsoleError(consoleMessage);
case ConsoleDataType.Info:
return logConsoleInfo(consoleMessage);
case ConsoleDataType.InProgress:
return logConsoleProgress(consoleMessage);
}
} }
private setNodePropertiesViewMode(viewMode: NodeProperties.Mode) { private setNodePropertiesViewMode(viewMode: NodeProperties.Mode) {
@@ -1368,7 +1380,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let { id } = d; let { id } = d;
if (typeof id !== "string") { if (typeof id !== "string") {
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`; const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); logConsoleError(error);
throw new Error(error); throw new Error(error);
} }
@@ -1380,7 +1392,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
pk = pk[0]["_value"]; pk = pk[0]["_value"];
} else { } else {
const error = `Vertex pk is not a string nor a non-empty array: ${JSON.stringify(pk)}.`; const error = `Vertex pk is not a string nor a non-empty array: ${JSON.stringify(pk)}.`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); logConsoleError(error);
throw new Error(error); throw new Error(error);
} }
} }
@@ -1767,7 +1779,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${ const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${
this.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE this.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE
})`; })`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`); const clearConsoleProgress = GraphExplorer.reportToConsole(
ConsoleDataType.InProgress,
`Executing: ${queryInfoStr}`
);
try { try {
const results: ViewModels.QueryResults = await queryDocumentsPage( const results: ViewModels.QueryResults = await queryDocumentsPage(
@@ -1776,7 +1791,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.currentDocDBQueryInfo.index this.currentDocDBQueryInfo.index
); );
GraphExplorer.clearConsoleProgress(id); clearConsoleProgress();
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
this.setState({ hasMoreRoots: results.hasMoreResults }); this.setState({ hasMoreRoots: results.hasMoreResults });
RU = results.requestCharge.toString(); RU = results.requestCharge.toString();
@@ -1793,7 +1808,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return { requestCharge: RU }; return { requestCharge: RU };
} catch (error) { } catch (error) {
GraphExplorer.clearConsoleProgress(id); clearConsoleProgress();
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`; const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({ this.setState({
@@ -2003,8 +2018,4 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
</React.Fragment> </React.Fragment>
); );
} }
private static clearConsoleProgress(id: string) {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}
} }

View File

@@ -3,11 +3,10 @@
*/ */
import * as Q from "q"; import * as Q from "q";
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../../Common/HashMap";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { HashMap } from "../../../Common/HashMap";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
export interface GremlinClientParameters { export interface GremlinClientParameters {
endpoint: string; endpoint: string;
@@ -77,9 +76,7 @@ export class GremlinClient {
this.abortPendingRequest(requestId, errorMessage, result.requestCharge); this.abortPendingRequest(requestId, errorMessage, result.requestCharge);
} }
}, },
infoCallback: (msg: string) => { infoCallback: logConsoleInfo,
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
},
}); });
} }

View File

@@ -1,75 +0,0 @@
import * as ko from "knockout";
import { NewVertexComponent, NewVertexViewModel } from "./NewVertexComponent";
const component = NewVertexComponent;
describe("New Vertex Component", () => {
let vm: NewVertexViewModel;
let partitionKeyProperty: ko.Observable<string>;
beforeEach(async () => {
document.body.innerHTML = component.template as any;
partitionKeyProperty = ko.observable(null);
vm = new component.viewModel({
newVertexData: null,
partitionKeyProperty,
});
ko.applyBindings(vm);
});
afterEach(() => {
ko.cleanNode(document);
});
describe("Rendering", () => {
it("should display property list with input and +Add Property", () => {
expect(document.querySelector(".newVertexComponent .newVertexForm")).not.toBeNull();
expect(document.querySelector(".newVertexComponent .edgeInput")).not.toBeNull();
expect(document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn")).not.toBeNull();
});
it("should display partition key property if set", () => {
partitionKeyProperty("testKey");
expect(
(document.querySelector(".newVertexComponent .newVertexForm .labelCol input") as HTMLInputElement).value
).toEqual("testKey");
});
it("should NOT display partition key property if NOT set", () => {
expect(document.getElementsByClassName("valueCol").length).toBe(0);
});
});
describe("Behavior", () => {
let clickSpy: jasmine.Spy;
beforeEach(() => {
clickSpy = jasmine.createSpy("Command button click spy");
});
it("should add new property row when +Add property button is pressed", () => {
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
expect(document.getElementsByClassName("valueCol").length).toBe(3);
expect(document.getElementsByClassName("rightPaneTrashIcon").length).toBe(3);
});
it("should remove property row when trash button is pressed", () => {
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
// Mark this one to delete
const elts = document.querySelectorAll(".newVertexComponent .rightPaneTrashIconImg");
elts[elts.length - 1].className += " deleteme";
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
document
.querySelector(".newVertexComponent .rightPaneTrashIconImg.deleteme")
.parentElement.dispatchEvent(new Event("click"));
expect(document.getElementsByClassName("valueCol").length).toBe(2);
expect(document.getElementsByClassName("rightPaneTrashIcon").length).toBe(2);
expect(document.querySelectorAll(".newVertexComponent .rightPaneTrashIconImg.deleteme").length).toBe(0);
});
});
});

View File

@@ -1,74 +0,0 @@
<div class="newVertexComponent" data-bind="setTemplateReady: true">
<div class="newVertexForm">
<div class="newVertexFormRow">
<label for="VertexLabel" class="labelCol">Label</label>
<input
class="edgeInput"
type="text"
data-bind="textInput:$data.newVertexData().label, hasFocus: $data.firstFieldHasFocus"
aria-label="Enter vertex label"
role="textbox"
tabindex="0"
placeholder="Enter vertex label"
autocomplete="off"
id="VertexLabel"
/>
<div class="actionCol"></div>
</div>
<!-- ko foreach:{ data:newVertexData().properties, as: 'property' } -->
<div class="newVertexFormRow">
<div class="labelCol">
<input
type="text"
id="propertyKeyNewVertexPane"
data-bind="textInput: property.key, attr: { 'aria-label': 'Enter key for property '+ ($index() + 1) }"
placeholder="Key"
autocomplete="off"
/>
</div>
<div class="valueCol">
<input
class="edgeInput"
type="text"
data-bind="textInput: property.values[0].value, , attr: { 'aria-label': 'Enter value for property '+ ($index() + 1) }"
placeholder="Value"
autocomplete="off"
/>
</div>
<div>
<select
class="typeSelect"
required
data-bind="options:$parent.propertyTypes, value:property.values[0].type, attr: { 'aria-label': property.values[0].type + ': for property '+ ($index() + 1) }"
></select>
</div>
<div class="actionCol">
<div
class="rightPaneTrashIcon rightPaneBtns"
data-bind="click:$parent.removeNewVertexProperty.bind($parent, $index()), event: { keypress: $parent.removeNewVertexPropertyKeyPress.bind($parent, $index()) }, attr: { 'aria-label': 'Remove property '+ ($index() + 1) }"
tabindex="0"
role="button"
>
<img class="refreshcol rightPaneTrashIconImg" src="/delete.svg" alt="Remove property" />
</div>
</div>
</div>
<!-- /ko -->
<div class="newVertexFormRow">
<span class="rightPaneAddPropertyBtnPadding">
<span
class="rightPaneAddPropertyBtn rightPaneBtns"
id="addProperyNewVertexBtn"
data-bind="click:onAddNewProperty, event: { keypress: onAddNewPropertyKeyPress }"
tabindex="0"
role="button"
>
<img class="refreshcol rightPaneAddPropertyImg" src="/Add-property.svg" alt="Add property" /> Add
Property</span
>
</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,97 @@
@import "../../../../less/Common/Constants";
.newVertexComponent {
padding: @LargeSpace 20px 20px 0px;
width: 400px;
.newVertexForm {
width: 100%;
.flex-display();
.flex-direction();
.newVertexFormRow {
.flex-display();
.flex-direction(@direction: row);
padding: 4px 5px;
label {
padding: 0px;
}
.valueCol {
flex-grow: 1;
padding-right: 5px;
}
.rightPaneAddPropertyBtnPadding {
padding-top: 14px;
}
.edgeLabel {
padding-right: 41px;
}
}
}
.actionCol {
min-width: 30px;
padding: 0px 4px;
}
.labelCol {
width: 72px;
min-width: 72px;
input {
max-width: 65px;
padding-left: 4px;
}
}
.edgeInput {
width: 100%;
padding-left: 4px;
}
.typeSelect {
height: 23px;
width: 70px;
}
.rightPaneTrashIcon {
padding: 4px 1px 0px 4px;
height: 100%;
}
.rightPaneTrashIconImg {
vertical-align: top;
}
.rightPaneAddPropertyBtn {
padding: 7px 7px 8px 8px;
margin-left: -8px;
}
.rightPaneBtns {
cursor: pointer;
&:hover {
background-color: @BaseLow;
}
&:active {
background-color: @AccentMediumLow;
}
}
.rightPaneAddPropertyImg {
margin-right: 5px;
margin-bottom: 4px;
}
.contentScroll {
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
}
}

View File

@@ -0,0 +1,118 @@
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { NewVertexComponent } from "./NewVertexComponent";
describe("New Vertex Component", () => {
beforeEach(() => {
const fakeNewVertexData: ViewModels.NewVertexData = {
label: "",
properties: [
{
key: "test1",
values: [
{
value: "",
type: "string",
},
],
},
],
};
const props = {
newVertexDataProp: fakeNewVertexData,
partitionKeyPropertyProp: "test1",
onChangeProp: (): void => undefined,
};
render(<NewVertexComponent {...props} />);
});
it("should render default prpoerty", () => {
const fakeNewVertexData: ViewModels.NewVertexData = {
label: "",
properties: [],
};
const props = {
newVertexDataProp: fakeNewVertexData,
partitionKeyPropertyProp: "",
onChangeProp: (): void => undefined,
};
const { asFragment } = render(<NewVertexComponent {...props} />);
expect(asFragment).toMatchSnapshot();
});
it("should render Add property button", () => {
const span = screen.getByText("Add Property");
expect(span).toBeDefined();
});
it("should call onAddNewProperty method on span click", () => {
const onAddNewProperty = jest.fn();
const span = screen.getByText("Add Property");
span.onclick = onAddNewProperty();
fireEvent.click(span);
expect(onAddNewProperty).toHaveBeenCalled();
});
it("should call onAddNewPropertyKeyPress method on span keyPress", () => {
const onAddNewPropertyKeyPress = jest.fn();
const span = screen.getByText("Add Property");
span.onkeypress = onAddNewPropertyKeyPress();
fireEvent.keyPress(span, { key: "Enter", code: 13, charCode: 13 });
expect(onAddNewPropertyKeyPress).toHaveBeenCalled();
});
it("should call onLabelChange method on input change", () => {
const onLabelChange = jest.fn();
const input = screen.getByLabelText("Label");
input.onchange = onLabelChange();
fireEvent.change(input, { target: { value: "Label" } });
expect(onLabelChange).toHaveBeenCalled();
});
it("should call onKeyChange method on key input change", () => {
const onKeyChange = jest.fn();
const input = screen.queryByPlaceholderText("Key");
input.onchange = onKeyChange();
fireEvent.change(input, { target: { value: "pk1" } });
expect(onKeyChange).toHaveBeenCalled();
});
it("should call onValueChange method on value input change", () => {
const onValueChange = jest.fn();
const input = screen.queryByPlaceholderText("Value");
input.onchange = onValueChange();
fireEvent.change(input, { target: { value: "abc" } });
expect(onValueChange).toHaveBeenCalled();
});
it("should call removeNewVertexProperty method on remove button click", () => {
const removeNewVertexProperty = jest.fn();
const div = screen.getAllByRole("button");
div[0].onclick = removeNewVertexProperty();
fireEvent.click(div[0]);
expect(removeNewVertexProperty).toHaveBeenCalled();
});
it("should call removeNewVertexProperty method on remove button keyPress", () => {
const removeNewVertexPropertyKeyPress = jest.fn();
const div = screen.getAllByRole("button");
div[0].onkeypress = removeNewVertexPropertyKeyPress();
fireEvent.keyPress(div[0], { key: "Enter", code: 13, charCode: 13 });
expect(removeNewVertexPropertyKeyPress).toHaveBeenCalled();
});
it("should call onTypeChange method on type dropdown change", () => {
const DOWN_ARROW = { keyCode: 40 };
const onTypeChange = jest.fn();
const dropdown = screen.getByRole("listbox");
dropdown.onclick = onTypeChange();
dropdown.onkeydown = onTypeChange();
fireEvent.keyDown(screen.getByRole("listbox"), DOWN_ARROW);
fireEvent.click(screen.getByText(/number/));
expect(onTypeChange).toHaveBeenCalled();
});
});

View File

@@ -1,99 +0,0 @@
import * as ko from "knockout";
import { EditorNodePropertiesComponent } from "../GraphExplorerComponent/EditorNodePropertiesComponent";
import { NewVertexData, InputProperty } from "../../../Contracts/ViewModels";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import * as Constants from "../../../Common/Constants";
import template from "./NewVertexComponent.html";
/**
* Parameters for this component
*/
export interface NewVertexParams {
// Data to be edited by the component
newVertexData: ko.Observable<NewVertexData>;
partitionKeyProperty: ko.Observable<string>;
firstFieldHasFocus?: ko.Observable<boolean>;
/**
* Callback triggered when the template is bound to the component (for testing purposes)
*/
onTemplateReady?: () => void;
}
export class NewVertexViewModel extends WaitsForTemplateViewModel {
private static readonly DEFAULT_PROPERTY_TYPE = "string";
private newVertexData: ko.Observable<NewVertexData>;
private firstFieldHasFocus: ko.Observable<boolean>;
private propertyTypes: string[];
public constructor(params: NewVertexParams) {
super();
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady && params.onTemplateReady) {
params.onTemplateReady();
}
});
this.newVertexData =
params.newVertexData ||
ko.observable({
label: "",
properties: <InputProperty[]>[],
});
this.firstFieldHasFocus = params.firstFieldHasFocus || ko.observable(false);
this.propertyTypes = EditorNodePropertiesComponent.VERTEX_PROPERTY_TYPES;
if (params.partitionKeyProperty) {
params.partitionKeyProperty.subscribe((newKeyProp: string) => {
if (!newKeyProp) {
return;
}
this.addNewVertexProperty(newKeyProp);
});
}
}
public onAddNewProperty() {
this.addNewVertexProperty();
document.getElementById("propertyKeyNewVertexPane").focus();
}
public onAddNewPropertyKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onAddNewProperty();
event.stopPropagation();
return false;
}
return true;
};
public addNewVertexProperty(key?: string) {
let ap = this.newVertexData().properties;
ap.push({ key: key || "", values: [{ value: "", type: NewVertexViewModel.DEFAULT_PROPERTY_TYPE }] });
this.newVertexData.valueHasMutated();
}
public removeNewVertexProperty(index: number) {
let ap = this.newVertexData().properties;
ap.splice(index, 1);
this.newVertexData.valueHasMutated();
document.getElementById("addProperyNewVertexBtn").focus();
}
public removeNewVertexPropertyKeyPress = (index: number, source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.removeNewVertexProperty(index);
event.stopPropagation();
return false;
}
return true;
};
}
/**
* Helper class for ko component registration
*/
export const NewVertexComponent = {
viewModel: NewVertexViewModel,
template,
};

View File

@@ -0,0 +1,213 @@
import { Dropdown, IDropdownOption, Stack, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useRef, useState } from "react";
import AddIcon from "../../../../images/Add-property.svg";
import DeleteIcon from "../../../../images/delete.svg";
import { NormalizedEventKey } from "../../../Common/Constants";
import { GremlinPropertyValueType, InputPropertyValueTypeString, NewVertexData } from "../../../Contracts/ViewModels";
import { EditorNodePropertiesComponent } from "../GraphExplorerComponent/EditorNodePropertiesComponent";
import "./NewVertexComponent.less";
export interface INewVertexComponentProps {
newVertexDataProp: NewVertexData;
partitionKeyPropertyProp: string;
onChangeProp: (labelData: NewVertexData) => void;
}
export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = ({
newVertexDataProp,
partitionKeyPropertyProp,
onChangeProp,
}: INewVertexComponentProps): JSX.Element => {
const DEFAULT_PROPERTY_TYPE = "string";
const [newVertexData, setNewVertexData] = useState<NewVertexData>(
newVertexDataProp || {
label: "",
properties: [
{
key: partitionKeyPropertyProp,
values: [{ value: "", type: DEFAULT_PROPERTY_TYPE }],
},
],
}
);
const propertyTypes: string[] = EditorNodePropertiesComponent.VERTEX_PROPERTY_TYPES;
const input = useRef(undefined);
const onAddNewProperty = () => {
addNewVertexProperty();
setTimeout(() => {
input.current.focus();
}, 100);
};
const onAddNewPropertyKeyPress = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
onAddNewProperty();
event.stopPropagation();
}
};
const addNewVertexProperty = () => {
let key: string;
const ap = newVertexData.properties;
if (ap.length === 0) {
key = partitionKeyPropertyProp;
}
ap.push({
key: key || "",
values: [{ value: "", type: DEFAULT_PROPERTY_TYPE }],
});
setNewVertexData((prevData) => ({
...prevData,
properties: ap,
}));
onChangeProp(newVertexData);
};
const removeNewVertexProperty = (event?: React.MouseEvent<HTMLDivElement>, index?: number) => {
const ap = newVertexData.properties;
ap.splice(index, 1);
setNewVertexData((prevData) => ({
...prevData,
properties: ap,
}));
onChangeProp(newVertexData);
document.getElementById("addProperyNewVertexBtn").focus();
};
const removeNewVertexPropertyKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, index: number) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
removeNewVertexProperty(undefined, index);
event.stopPropagation();
}
};
const onLabelChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setNewVertexData((prevData) => ({
...prevData,
label: event.target.value,
}));
onChangeProp(newVertexData);
};
const onKeyChange = (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
const newState = { ...newVertexData };
newState.properties[index].key = event.target.value;
setNewVertexData(newState);
onChangeProp(newVertexData);
};
const onValueChange = (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
const newState = { ...newVertexData };
newState.properties[index].values[0].value = event.target.value as GremlinPropertyValueType;
setNewVertexData(newState);
onChangeProp(newVertexData);
};
const onTypeChange = (option: string, index: number) => {
const newState = { ...newVertexData };
if (newState.properties[index]) {
newState.properties[index].values[0].type = option as InputPropertyValueTypeString;
setNewVertexData(newState);
onChangeProp(newVertexData);
}
};
return (
<Stack>
<div className="newVertexComponent">
<div className="newVertexForm">
<div className="newVertexFormRow">
<TextField
label="Label"
className="edgeInput"
type="text"
ariaLabel="Enter vertex label"
role="textbox"
tabIndex={0}
placeholder="Enter vertex label"
autoComplete="off"
id="VertexLabel"
value={newVertexData.label}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onLabelChange(event);
}}
/>
<div className="actionCol"></div>
</div>
{newVertexData.properties.map((data, index) => {
return (
<div key={index} className="newVertexFormRow">
<div className="labelCol">
<TextField
className="edgeInput"
type="text"
id="propertyKeyNewVertexPane"
componentRef={input}
placeholder="Key"
autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`}
value={data.key}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
/>
</div>
<div className="valueCol">
<TextField
className="edgeInput"
type="text"
placeholder="Value"
autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`}
value={data.values[0].value.toString()}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onValueChange(event, index)}
/>
</div>
<div>
<Dropdown
role="listbox"
placeholder="Select an option"
defaultSelectedKey={data.values[0].type}
style={{ width: 100 }}
options={propertyTypes.map((type) => ({
key: type,
text: type,
}))}
onChange={(_, options: IDropdownOption) => onTypeChange(options.key.toString(), index)}
/>
</div>
<div className="actionCol">
<div
className="rightPaneTrashIcon rightPaneBtns"
tabIndex={0}
role="button"
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
removeNewVertexPropertyKeyPress(event, index)
}
>
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" />
</div>
</div>
</div>
);
})}
<div className="newVertexFormRow">
<span className="rightPaneAddPropertyBtnPadding">
<span
className="rightPaneAddPropertyBtn rightPaneBtns"
id="addProperyNewVertexBtn"
tabIndex={0}
role="button"
onClick={onAddNewProperty}
onKeyPress={(event: React.KeyboardEvent<HTMLSpanElement>) => onAddNewPropertyKeyPress(event)}
>
<img className="refreshcol rightPaneAddPropertyImg" src={AddIcon} alt="Add property" /> Add Property
</span>
</span>
</div>
</div>
</div>
</Stack>
);
};

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`New Vertex Component should render default prpoerty 1`] = `[Function]`;

View File

@@ -1,97 +0,0 @@
@import "../../../../less/Common/Constants";
.newVertexComponent {
padding: @LargeSpace 20px 20px 0px;
width: 400px;
.newVertexForm {
width: 100%;
.flex-display();
.flex-direction();
.newVertexFormRow {
.flex-display();
.flex-direction(@direction: row);
padding: 4px 5px;
label {
padding: 0px;
}
.valueCol {
flex-grow: 1;
padding-right: 5px;
}
.rightPaneAddPropertyBtnPadding {
padding-top: 14px;
}
.edgeLabel {
padding-right: 41px;
}
}
}
.actionCol {
min-width: 30px;
padding: 0px 4px;
}
.labelCol {
width: 72px;
min-width: 72px;
input {
max-width: 65px;
padding-left: 4px;
}
}
.edgeInput {
width: 100%;
padding-left: 4px;
}
.typeSelect {
height: 23px;
width: 70px;
}
.rightPaneTrashIcon {
padding: 4px 1px 0px 4px;
height: 100%;
}
.rightPaneTrashIconImg {
vertical-align: top;
}
.rightPaneAddPropertyBtn {
padding: 7px 7px 8px 8px;
margin-left: -8px;
}
.rightPaneBtns {
cursor: pointer;
&:hover {
background-color: @BaseLow ;
}
&:active {
background-color: @AccentMediumLow;
}
}
.rightPaneAddPropertyImg {
margin-right: 5px;
margin-bottom: 4px;
}
.contentScroll {
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
}
}

View File

@@ -23,16 +23,13 @@ export class CommandBarComponentAdapter implements ReactAdapter {
constructor(container: Explorer) { constructor(container: Explorer) {
this.container = container; this.container = container;
this.tabsButtons = []; this.tabsButtons = [];
this.isNotebookTabActive = ko.computed(() => this.isNotebookTabActive = ko.computed(
container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2) () => container.tabsManager.activeTab()?.tabKind === ViewModels.CollectionTabKind.NotebookV2
); );
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates // These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [ const toWatch = [
container.isPreferredApiTable,
container.isPreferredApiMongoDB, container.isPreferredApiMongoDB,
container.isPreferredApiCassandra,
container.isPreferredApiGraph,
container.deleteCollectionText, container.deleteCollectionText,
container.deleteDatabaseText, container.deleteDatabaseText,
container.addCollectionText, container.addCollectionText,

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { AuthType } from "../../../AuthType"; import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -15,9 +16,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -54,9 +60,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -118,8 +129,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); updateUserContext({
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -197,8 +213,15 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addDatabaseText = ko.observable("mockText");
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
@@ -208,15 +231,27 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
beforeEach(() => { beforeEach(() => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableCassandra" }],
},
} as DatabaseAccount,
});
mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false);
}); });
it("Cassandra Api not available - button should be hidden", () => { it("Cassandra Api not available - button should be hidden", () => {
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
console.log(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined(); expect(openCassandraShellBtn).toBeUndefined();
@@ -279,9 +314,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isPreferredApiTable = ko.computed(() => true); updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
@@ -346,6 +386,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
it("should only show New SQL Query and Open Query buttons", () => { it("should only show New SQL Query and Open Query buttons", () => {
updateUserContext({
databaseAccount: {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
expect(buttons.length).toBe(2); expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query"); expect(buttons[0].commandButtonLabel).toBe("New SQL Query");

View File

@@ -47,7 +47,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(addSynapseLink); buttons.push(addSynapseLink);
} }
if (!container.isPreferredApiTable()) { if (userContext.apiType !== "Tables") {
newCollectionBtn.children = [createNewCollectionGroup(container)]; newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container); const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn); newCollectionBtn.children.push(newDatabaseBtn);
@@ -74,7 +74,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenMongoTerminalButton(container)); buttons.push(createOpenMongoTerminalButton(container));
} }
if (container.isPreferredApiCassandra()) { if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container)); buttons.push(createOpenCassandraTerminalButton(container));
} }
} }
@@ -90,15 +90,15 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createDivider()); buttons.push(createDivider());
} }
const isSqlQuerySupported = userContext.apiType === "SQL" || container.isPreferredApiGraph(); const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSqlQuerySupported) { if (isSqlQuerySupported) {
const newSqlQueryBtn = createNewSQLQueryButton(container); const newSqlQueryBtn = createNewSQLQueryButton(container);
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }
const isSupportedOpenQueryApi = const isSupportedOpenQueryApi =
userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || container.isPreferredApiGraph(); userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin";
const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || container.isPreferredApiGraph(); const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) { if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container); const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
@@ -107,7 +107,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createOpenQueryFromDiskButton(container)); buttons.push(createOpenQueryFromDiskButton(container));
} }
if (areScriptsSupported(container)) { if (areScriptsSupported()) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = { const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon, iconSrc: AddStoredProcedureIcon,
@@ -216,8 +216,8 @@ export function createDivider(): CommandButtonComponentProps {
}; };
} }
function areScriptsSupported(container: Explorer): boolean { function areScriptsSupported(): boolean {
return userContext.apiType === "SQL" || container.isPreferredApiGraph(); return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
} }
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps { function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
@@ -289,7 +289,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
} }
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps { function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (userContext.apiType === "SQL" || container.isPreferredApiGraph()) { if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query"; const label = "New SQL Query";
return { return {
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
@@ -303,7 +303,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
hasPopup: true, hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(), disabled: container.isDatabaseNodeOrNoneSelected(),
}; };
} else if (container.isPreferredApiMongoDB()) { } else if (userContext.apiType === "Mongo") {
const label = "New Query"; const label = "New Query";
return { return {
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
@@ -325,8 +325,7 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] { export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean = const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
!container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(container);
if (shouldEnableScriptsCommands) { if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure"; const label = "New Stored Procedure";
@@ -446,7 +445,7 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
return { return {
iconSrc: EnableNotebooksIcon, iconSrc: EnableNotebooksIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.setupNotebooksPane.openWithTitleAndDescription(label, description), onCommandClick: () => container.openSetupNotebooksPanel(label, description),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: !container.isNotebooksEnabledForAccount(), disabled: !container.isNotebooksEnabledForAccount(),
@@ -483,7 +482,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
if (container.isNotebookEnabled()) { if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else { } else {
container.setupNotebooksPane.openWithTitleAndDescription(title, description); container.openSetupNotebooksPanel(title, description);
} }
}, },
commandButtonLabel: label, commandButtonLabel: label,
@@ -509,7 +508,7 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
if (container.isNotebookEnabled()) { if (container.isNotebookEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra); container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else { } else {
container.setupNotebooksPane.openWithTitleAndDescription(title, description); container.openSetupNotebooksPanel(title, description);
} }
}, },
commandButtonLabel: label, commandButtonLabel: label,

View File

@@ -34,8 +34,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import configureStore from "./NotebookComponent/store"; import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types"; import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
import SandboxJavaScript from "./NotebookRenderer/outputs/SandboxJavaScript";
import SanitizedHTML from "./NotebookRenderer/outputs/SanitizedHTML";
export type KernelSpecsDisplay = { name: string; displayName: string }; export type KernelSpecsDisplay = { name: string; displayName: string };
@@ -125,62 +123,62 @@ export class NotebookClientV2 {
contents: makeContentsRecord({ contents: makeContentsRecord({
byRef: Immutable.Map<string, ContentRecord>(), byRef: Immutable.Map<string, ContentRecord>(),
}), }),
transforms: makeTransformsRecord({ transforms: userContext.features.sandboxNotebookOutputs
displayOrder: Immutable.List([ ? undefined
"application/vnd.jupyter.widget-view+json", : makeTransformsRecord({
"application/vnd.vega.v5+json", displayOrder: Immutable.List([
"application/vnd.vega.v4+json", "application/vnd.jupyter.widget-view+json",
"application/vnd.vega.v3+json", "application/vnd.vega.v5+json",
"application/vnd.vega.v2+json", "application/vnd.vega.v4+json",
"application/vnd.vegalite.v3+json", "application/vnd.vega.v3+json",
"application/vnd.vegalite.v2+json", "application/vnd.vega.v2+json",
"application/vnd.vegalite.v1+json", "application/vnd.vegalite.v3+json",
"application/geo+json", "application/vnd.vegalite.v2+json",
"application/vnd.plotly.v1+json", "application/vnd.vegalite.v1+json",
"text/vnd.plotly.v1+html", "application/geo+json",
"application/x-nteract-model-debug+json", "application/vnd.plotly.v1+json",
"application/vnd.dataresource+json", "text/vnd.plotly.v1+html",
"application/vdom.v1+json", "application/x-nteract-model-debug+json",
"application/json", "application/vnd.dataresource+json",
"application/javascript", "application/vdom.v1+json",
"text/html", "application/json",
"text/markdown", "application/javascript",
"text/latex", "text/html",
"image/svg+xml", "text/markdown",
"image/gif", "text/latex",
"image/png", "image/svg+xml",
"image/jpeg", "image/gif",
"text/plain", "image/png",
]), "image/jpeg",
byId: Immutable.Map({ "text/plain",
"text/vnd.plotly.v1+html": NullTransform, ]),
"application/vnd.plotly.v1+json": NullTransform, byId: Immutable.Map({
"application/geo+json": NullTransform, "text/vnd.plotly.v1+html": NullTransform,
"application/x-nteract-model-debug+json": NullTransform, "application/vnd.plotly.v1+json": NullTransform,
"application/vnd.dataresource+json": NullTransform, "application/geo+json": NullTransform,
"application/vnd.jupyter.widget-view+json": NullTransform, "application/x-nteract-model-debug+json": NullTransform,
"application/vnd.vegalite.v1+json": NullTransform, "application/vnd.dataresource+json": NullTransform,
"application/vnd.vegalite.v2+json": NullTransform, "application/vnd.jupyter.widget-view+json": NullTransform,
"application/vnd.vegalite.v3+json": NullTransform, "application/vnd.vegalite.v1+json": NullTransform,
"application/vnd.vega.v2+json": NullTransform, "application/vnd.vegalite.v2+json": NullTransform,
"application/vnd.vega.v3+json": NullTransform, "application/vnd.vegalite.v3+json": NullTransform,
"application/vnd.vega.v4+json": NullTransform, "application/vnd.vega.v2+json": NullTransform,
"application/vnd.vega.v5+json": NullTransform, "application/vnd.vega.v3+json": NullTransform,
"application/vdom.v1+json": TransformVDOM, "application/vnd.vega.v4+json": NullTransform,
"application/json": Media.Json, "application/vnd.vega.v5+json": NullTransform,
"application/javascript": userContext.features.sandboxNotebookOutputs "application/vdom.v1+json": TransformVDOM,
? SandboxJavaScript "application/json": Media.Json,
: Media.JavaScript, "application/javascript": Media.JavaScript,
"text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : Media.HTML, "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,
"image/gif": Media.Image, "image/gif": Media.Image,
"image/png": Media.Image, "image/png": Media.Image,
"image/jpeg": Media.Image, "image/jpeg": Media.Image,
"text/plain": Media.Plain, "text/plain": Media.Plain,
}), }),
}), }),
}), }),
}), }),
cdb: makeCdbRecord({ cdb: makeCdbRecord({
@@ -202,10 +200,11 @@ export class NotebookClientV2 {
case actions.FETCH_KERNELSPECS_FULFILLED: { case actions.FETCH_KERNELSPECS_FULFILLED: {
const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload; const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload;
const defaultKernelName = payload.defaultKernelName; const defaultKernelName = payload.defaultKernelName;
this.kernelSpecsForDisplay = Object.keys(payload.kernelspecs) this.kernelSpecsForDisplay = Object.values(payload.kernelspecs)
.map((name) => ({ .filter((spec) => !spec.metadata?.hasOwnProperty("hidden"))
name, .map((spec) => ({
displayName: payload.kernelspecs[name].displayName, name: spec.name,
displayName: spec.displayName,
})) }))
.sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => { .sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => {
// Put default at the top, otherwise lexicographically compare // Put default at the top, otherwise lexicographically compare

View File

@@ -1,52 +1,49 @@
import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer, from } from "rxjs";
import { webSocket } from "rxjs/webSocket";
import { StateObservable } from "redux-observable";
import { ofType } from "redux-observable";
import { import {
mergeMap,
tap,
retryWhen,
delayWhen,
map,
switchMap,
take,
filter,
catchError,
first,
concatMap,
timeout,
} from "rxjs/operators";
import {
AppState,
ServerConfig as JupyterServerConfig,
JupyterHostRecordProps,
RemoteKernelProps,
castToSessionId,
createKernelRef,
KernelRef,
ContentRef,
KernelInfo,
actions, actions,
AppState,
castToSessionId,
ContentRef,
createKernelRef,
JupyterHostRecordProps,
KernelInfo,
KernelRef,
RemoteKernelProps,
selectors, selectors,
ServerConfig as JupyterServerConfig,
} from "@nteract/core"; } from "@nteract/core";
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging"; import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable"; import { RecordOf } from "immutable";
import { AnyAction } from "redux"; import { AnyAction } from "redux";
import { ofType, StateObservable } from "redux-observable";
import { kernels, sessions } from "rx-jupyter";
import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
import {
catchError,
concatMap,
delayWhen,
filter,
first,
map,
mergeMap,
retryWhen,
switchMap,
take,
tap,
timeout,
} from "rxjs/operators";
import { webSocket } from "rxjs/webSocket";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import { Areas } from "../../../Common/Constants";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { CdbAppState } from "./types"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils"; import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { NotebookUtil } from "../NotebookUtil";
import * as FileSystemUtil from "../FileSystemUtil"; import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { Areas } from "../../../Common/Constants"; import { NotebookUtil } from "../NotebookUtil";
import * as CdbActions from "./actions";
import * as TextFile from "./contents/file/text-file";
import { CdbAppState } from "./types";
interface NotebookServiceConfig extends JupyterServerConfig { interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string; userPuid?: string;
@@ -311,7 +308,7 @@ export const launchWebSocketKernelEpic = (
if (currentKernelspecs) { if (currentKernelspecs) {
kernelSpecToLaunch = currentKernelspecs.defaultKernelName; kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`; const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg); logConsoleInfo(msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg); logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
} else { } else {
return of( return of(
@@ -337,7 +334,7 @@ export const launchWebSocketKernelEpic = (
kernelSpecToLaunch = currentKernelspecs.defaultKernelName; kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
msg += ` Using default kernel: ${kernelSpecToLaunch}`; msg += ` Using default kernel: ${kernelSpecToLaunch}`;
} }
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg); logConsoleInfo(msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg); logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
} }
@@ -634,7 +631,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
case actions.RESTART_KERNEL_SUCCESSFUL: { case actions.RESTART_KERNEL_SUCCESSFUL: {
const title = "Kernel restart"; const title = "Kernel restart";
const msg = "Kernel successfully restarted"; const msg = "Kernel successfully restarted";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg); logConsoleInfo(msg);
logFailureToTelemetry(state$.value, title, msg); logFailureToTelemetry(state$.value, title, msg);
break; break;
} }
@@ -645,7 +642,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
case actions.SAVE_FAILED: { case actions.SAVE_FAILED: {
const title = "Save failure"; const title = "Save failure";
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`; const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); logConsoleError(msg);
logFailureToTelemetry(state$.value, title, msg); logFailureToTelemetry(state$.value, title, msg);
break; break;
} }
@@ -654,7 +651,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const filepath = selectors.filepath(state$.value, { contentRef: typedAction.payload.contentRef }); const filepath = selectors.filepath(state$.value, { contentRef: typedAction.payload.contentRef });
const title = "Fetching content failure"; const title = "Fetching content failure";
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`; const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); logConsoleError(msg);
logFailureToTelemetry(state$.value, title, msg); logFailureToTelemetry(state$.value, title, msg);
break; break;
} }
@@ -679,7 +676,7 @@ const handleKernelConnectionLostEpic = (
const state = state$.value; const state = state$.value;
const msg = "Notebook was disconnected from kernel"; const msg = "Notebook was disconnected from kernel";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); logConsoleError(msg);
logFailureToTelemetry(state, "Error", "Kernel connection error"); logFailureToTelemetry(state, "Error", "Kernel connection error");
const host = selectors.currentHost(state); const host = selectors.currentHost(state);
@@ -692,7 +689,7 @@ const handleKernelConnectionLostEpic = (
if (delayMs > Constants.Notebook.kernelRestartMaxDelayMs) { if (delayMs > Constants.Notebook.kernelRestartMaxDelayMs) {
const msg = const msg =
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically."; "Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); logConsoleError(msg);
logFailureToTelemetry(state, "Kernel restart error", msg); logFailureToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer; const explorer = window.dataExplorer;
@@ -810,7 +807,7 @@ const closeUnsupportedMimetypesEpic = (
); );
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg); explorer.showOkModalDialog("File cannot be rendered", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); logConsoleError(msg);
} }
return EMPTY; return EMPTY;
}) })
@@ -838,7 +835,7 @@ const closeContentFailedToFetchEpic = (
); );
const msg = `Failed to load file: ${filepath}.`; const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg); explorer.showOkModalDialog("Failure to load", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); logConsoleError(msg);
} }
return EMPTY; return EMPTY;
}) })

View File

@@ -1,15 +1,14 @@
/** /**
* Notebook container related stuff * Notebook container related stuff
*/ */
import * as DataModels from "../../Contracts/DataModels";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as Logger from "../../Common/Logger";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
export class NotebookContainerClient { export class NotebookContainerClient {
private reconnectingNotificationId: string; private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean; private isResettingWorkspace: boolean;
constructor( constructor(
@@ -61,9 +60,9 @@ export class NotebookContainerClient {
}, },
}); });
if (response.ok) { if (response.ok) {
if (this.reconnectingNotificationId) { if (this.clearReconnectionAttemptMessage) {
NotificationConsoleUtils.clearInProgressMessageWithId(this.reconnectingNotificationId); this.clearReconnectionAttemptMessage();
this.reconnectingNotificationId = ""; this.clearReconnectionAttemptMessage = undefined;
} }
const memoryUsageInfo = await response.json(); const memoryUsageInfo = await response.json();
if (memoryUsageInfo) { if (memoryUsageInfo) {
@@ -76,9 +75,8 @@ export class NotebookContainerClient {
return undefined; return undefined;
} catch (error) { } catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage"); Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.reconnectingNotificationId) { if (!this.clearReconnectionAttemptMessage) {
this.reconnectingNotificationId = NotificationConsoleUtils.logConsoleMessage( this.clearReconnectionAttemptMessage = logConsoleProgress(
ConsoleDataType.InProgress,
"Connection lost with Notebook server. Attempting to reconnect..." "Connection lost with Notebook server. Attempting to reconnect..."
); );
} }

View File

@@ -2,30 +2,31 @@
* Contains all notebook related stuff meant to be dynamically loaded by explorer * Contains all notebook related stuff meant to be dynamically loaded by explorer
*/ */
import { JunoClient } from "../../Juno/JunoClient";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { GitHubClient } from "../../GitHub/GitHubClient";
import * as Logger from "../../Common/Logger";
import { HttpStatusCodes, Areas } from "../../Common/Constants";
import { GitHubReposPane } from "../Panes/GitHubReposPane";
import ko from "knockout";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { IContentProvider } from "@nteract/core";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable"; import { ImmutableNotebook } from "@nteract/commutable";
import { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react";
import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { getFullName } from "../../Utils/UserUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { GitHubReposPane } from "../Panes/GitHubReposPane";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
@@ -49,7 +50,6 @@ export default class NotebookManager {
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter; public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
public initialize(params: NotebookManagerOptions): void { public initialize(params: NotebookManagerOptions): void {
this.params = params; this.params = params;
@@ -89,12 +89,6 @@ export default class NotebookManager {
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient); this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
this.params.container,
this.junoClient,
this.gitHubOAuthService
);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => { this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token); this.gitHubClient.setToken(token?.access_token);
@@ -129,7 +123,18 @@ export default class NotebookManager {
} }
public openCopyNotebookPane(name: string, content: string): void { public openCopyNotebookPane(name: string, content: string): void {
this.copyNotebookPaneAdapter.open(name, content); const { container } = this.params;
container.openSidePanel(
"Copy Notebook",
<CopyNotebookPane
container={container}
closePanel={container.closeSidePanel}
junoClient={this.junoClient}
gitHubOAuthService={this.gitHubOAuthService}
name={name}
content={content}
/>
);
} }
// Octokit's error handler uses any // Octokit's error handler uses any

View File

@@ -1,10 +1,8 @@
import { actions, ContentRef } from "@nteract/core"; import { actions, ContentRef } from "@nteract/core";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components"; import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt"; import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
@@ -14,7 +12,7 @@ import { AzureTheme } from "./AzureTheme";
import "./base.css"; import "./base.css";
import "./default.css"; import "./default.css";
import "./NotebookReadOnlyRenderer.less"; import "./NotebookReadOnlyRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs"; import SandboxOutputs from "./outputs/SandboxOutputs";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: any;
@@ -27,7 +25,9 @@ export interface NotebookRendererProps {
*/ */
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> { class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() { componentDidMount() {
loadTransform(this.props as any); if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any);
}
} }
private renderPrompt(id: string, contentRef: string): JSX.Element { private renderPrompt(id: string, contentRef: string): JSX.Element {
@@ -63,14 +63,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
{{ {{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef), prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
outputs: userContext.features.sandboxNotebookOutputs outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => ( ? () => <SandboxOutputs id={id} contentRef={contentRef} />
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined, : undefined,
editor: { editor: {
monaco: (props: PassedEditorProps) => monaco: (props: PassedEditorProps) =>

View File

@@ -1,11 +1,9 @@
import { CellId } from "@nteract/commutable"; import { CellId } from "@nteract/commutable";
import { CellType } from "@nteract/commutable/src"; import { CellType } from "@nteract/commutable/src";
import { actions, ContentRef } from "@nteract/core"; import { actions, ContentRef } from "@nteract/core";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components"; import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import * as React from "react"; import * as React from "react";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend"; import HTML5Backend from "react-dnd-html5-backend";
@@ -23,7 +21,7 @@ import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import "./default.css"; import "./default.css";
import MarkdownCell from "./markdown-cell"; import MarkdownCell from "./markdown-cell";
import "./NotebookRenderer.less"; import "./NotebookRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs"; import SandboxOutputs from "./outputs/SandboxOutputs";
import Prompt from "./Prompt"; import Prompt from "./Prompt";
import { promptContent } from "./PromptContent"; import { promptContent } from "./PromptContent";
import StatusBar from "./StatusBar"; import StatusBar from "./StatusBar";
@@ -71,7 +69,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
} }
componentDidMount() { componentDidMount() {
loadTransform(this.props as any); if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any);
}
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current); this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
} }
@@ -109,14 +109,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
), ),
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />, toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
outputs: userContext.features.sandboxNotebookOutputs outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => ( ? () => <SandboxOutputs id={id} contentRef={contentRef} />
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined, : undefined,
}} }}
</CodeCell> </CodeCell>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,132 @@
import { JSONObject } from "@nteract/commutable";
import { outputToJS } from "@nteract/commutable/lib/v4";
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import IframeResizer from "iframe-resizer-react";
import Immutable from "immutable";
import postRobot from "post-robot";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { CellOutputViewerProps } from "../../../../CellOutputViewer/CellOutputViewer";
// Adapted from https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/outputs/index.tsx
// to add support for sandboxing using <iframe>
interface ComponentProps {
id: string;
contentRef: ContentRef;
}
interface StateProps {
hidden: boolean;
expanded: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outputs: Immutable.List<any>;
}
interface DispatchProps {
onMetadataChange?: (metadata: JSONObject, mediaType: string, index?: number) => void;
}
export class SandboxOutputs extends React.PureComponent<ComponentProps & StateProps & DispatchProps> {
private childWindow: Window;
render(): JSX.Element {
// Using min-width to set the width of the iFrame, works around an issue in iOS that can prevent the iFrame from sizing correctly.
return (
<IframeResizer
checkOrigin={false}
loading="lazy"
heightCalculationMethod="taggedElement"
onLoad={(event) => this.handleFrameLoad(event)}
src="./cellOutputViewer.html"
style={{ height: "1px", width: "1px", minWidth: "100%", border: "none" }}
sandbox="allow-downloads allow-popups allow-forms allow-pointer-lock allow-scripts allow-popups-to-escape-sandbox"
/>
);
}
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
this.childWindow = (event.target as HTMLIFrameElement).contentWindow;
this.sendPropsToFrame();
}
sendPropsToFrame(): void {
if (!this.childWindow) {
return;
}
const props: CellOutputViewerProps = {
id: this.props.id,
contentRef: this.props.contentRef,
hidden: this.props.hidden,
expanded: this.props.expanded,
outputs: this.props.outputs.toArray().map((output) => outputToJS(output)),
onMetadataChange: this.props.onMetadataChange,
};
postRobot.send(this.childWindow, "props", props);
}
componentDidMount(): void {
this.sendPropsToFrame();
}
componentDidUpdate(): void {
this.sendPropsToFrame();
}
}
export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: AppState): StateProps => {
let outputs = Immutable.List();
let hidden = false;
let expanded = false;
const { contentRef, id } = ownProps;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
hidden = cell.cell_type === "code" && cell.getIn(["metadata", "jupyter", "outputs_hidden"]);
expanded = cell.cell_type === "code" && cell.getIn(["metadata", "collapsed"]) === false;
}
}
return { outputs, hidden, expanded };
};
return mapStateToProps;
};
export const makeMapDispatchToProps = (
initialDispath: Dispatch,
ownProps: ComponentProps
): ((dispatch: Dispatch) => DispatchProps) => {
const { id, contentRef } = ownProps;
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => {
dispatch(
actions.updateOutputMetadata({
id,
contentRef,
metadata,
index: index || 0,
mediaType,
})
);
},
};
};
return mapDispatchToProps;
};
export default connect<StateProps, DispatchProps, ComponentProps, AppState>(
makeMapStateToProps,
makeMapDispatchToProps
)(SandboxOutputs);

View File

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

View File

@@ -0,0 +1,10 @@
.shemaAnalyzerComponent {
width: 100%;
height: 100%;
overflow-y: auto;
}
.schemaAnalyzerCard {
max-width: 4096px;
width: 100%;
}

View File

@@ -0,0 +1,238 @@
import { ImmutableOutput } from "@nteract/commutable";
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
import { KernelOutputError, Output, StreamText } from "@nteract/outputs";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import { Card } from "@uifabric/react-cards";
import Immutable from "immutable";
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "office-ui-fabric-react";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import loadTransform from "../NotebookComponent/loadTransform";
import "./SchemaAnalyzerComponent.less";
interface SchemaAnalyzerComponentPureProps {
contentRef: ContentRef;
kernelRef: KernelRef;
databaseId: string;
collectionId: string;
}
interface SchemaAnalyzerComponentDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
}
type OutputType = "rich" | "json";
interface SchemaAnalyzerComponentState {
outputType: OutputType;
filter?: string;
isFiltering: boolean;
}
type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps &
StateProps &
SchemaAnalyzerComponentDispatchProps;
export class SchemaAnalyzerComponent extends React.Component<
SchemaAnalyzerComponentProps,
SchemaAnalyzerComponentState
> {
constructor(props: SchemaAnalyzerComponentProps) {
super(props);
this.state = {
outputType: "rich",
isFiltering: false,
};
}
componentDidMount(): void {
loadTransform(this.props);
}
private onFilterTextFieldChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.setState({
filter: newValue,
});
};
private onAnalyzeButtonClick = () => {
const query = {
command: "listSchema",
database: this.props.databaseId,
collection: this.props.collectionId,
outputType: this.state.outputType,
filter: this.state.filter,
};
if (this.state.filter) {
this.setState({
isFiltering: true,
});
}
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
this.props.runCell(this.props.contentRef, this.props.firstCellId);
};
render(): JSX.Element {
const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props;
if (!id) {
return <></>;
}
const isKernelBusy = kernelStatus === "busy";
const isKernelIdle = kernelStatus === "idle";
const showSchemaOutput = isKernelIdle && outputs.size > 0;
return (
<Stack className="schemaAnalyzerComponent" horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item grow styles={{ root: { display: "contents" } }}>
<Stack horizontal tokens={{ childrenGap: 20 }} styles={{ root: { width: "100%" } }}>
<Stack.Item grow align="end">
<TextField
value={this.state.filter}
onChange={this.onFilterTextFieldChange}
label="Filter"
placeholder="{ field: 'value' }"
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={this.onAnalyzeButtonClick}
disabled={!isKernelIdle}
/>
</Stack.Item>
</Stack>
</Stack.Item>
{showSchemaOutput ? (
outputs.map((output, index) => (
<Card className="schemaAnalyzerCard" key={index}>
<Card.Item tokens={{ padding: 10 }}>
<Output output={output}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</Output>
</Card.Item>
</Card>
))
) : this.state.isFiltering ? (
<Stack.Item>
{isKernelBusy && <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />}
</Stack.Item>
) : (
<>
<Stack.Item>
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} />
</Stack.Item>
<Stack.Item>
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={this.onAnalyzeButtonClick}
disabled={kernelStatus !== "idle"}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</>
)}
</Stack>
);
}
}
interface StateProps {
firstCellId: string;
kernelStatus: string;
outputs: Immutable.List<ImmutableOutput>;
}
interface InitialProps {
kernelRef: string;
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const { kernelRef, contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
let kernelStatus;
let firstCellId;
let outputs;
const kernel = selectors.kernel(state, { kernelRef });
if (kernel) {
kernelStatus = kernel.status;
}
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const cellOrder = selectors.notebook.cellOrder(content.model);
if (cellOrder.size > 0) {
firstCellId = cellOrder.first() as string;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id: firstCellId });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
}
}
}
}
return {
firstCellId,
kernelStatus,
outputs,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
return dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
})
);
},
runCell: (contentRef: ContentRef, cellId: string) => {
return dispatch(
actions.executeCell({
contentRef,
id: cellId,
})
);
},
updateCell: (text: string, id: string, contentRef: ContentRef) => {
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
},
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent);

View File

@@ -0,0 +1,88 @@
import { Notebook } from "@nteract/commutable";
import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core";
import * as React from "react";
import { Provider } from "react-redux";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import SchemaAnalyzerComponent from "./SchemaAnalyzerComponent";
export class SchemaAnalyzerComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
const notebook: Notebook = {
cells: [
{
cell_type: "code",
metadata: {},
execution_count: 0,
outputs: [],
source: "",
},
],
metadata: {
kernelspec: {
displayName: "Mongo",
language: "mongocli",
name: "mongo",
},
language_info: {
file_extension: "ipynb",
mimetype: "application/json",
name: "mongo",
version: "1.0",
},
},
nbformat: 4,
nbformat_minor: 4,
};
const model: IContent<"notebook"> = {
name: "schema-analyzer-component-notebook.ipynb",
path: "schema-analyzer-component-notebook.ipynb",
type: "notebook",
writable: true,
created: "",
last_modified: "",
mimetype: "application/x-ipynb+json",
content: notebook,
format: "json",
};
// Request fetching notebook content
this.getStore().dispatch(
actions.fetchContentFulfilled({
filepath: model.path,
model,
kernelRef: this.kernelRef,
contentRef: this.contentRef,
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId,
};
return (
<Provider store={this.getStore()}>
<SchemaAnalyzerComponent {...props} />;
</Provider>
);
}
}

View File

@@ -1,10 +1,10 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { handleOpenAction } from "./OpenActions";
import * as ViewModels from "../Contracts/ViewModels";
import { ActionContracts } from "../Contracts/ExplorerContracts"; import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import { handleOpenAction } from "./OpenActions";
import AddCollectionPane from "./Panes/AddCollectionPane"; import AddCollectionPane from "./Panes/AddCollectionPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
describe("OpenActions", () => { describe("OpenActions", () => {
describe("handleOpenAction", () => { describe("handleOpenAction", () => {
@@ -33,6 +33,7 @@ describe("OpenActions", () => {
collection.expandCollection = jest.fn(); collection.expandCollection = jest.fn();
collection.onDocumentDBDocumentsClick = jest.fn(); collection.onDocumentDBDocumentsClick = jest.fn();
collection.onMongoDBDocumentsClick = jest.fn(); collection.onMongoDBDocumentsClick = jest.fn();
collection.onSchemaAnalyzerClick = jest.fn();
collection.onTableEntitiesClick = jest.fn(); collection.onTableEntitiesClick = jest.fn();
collection.onGraphDocumentsClick = jest.fn(); collection.onGraphDocumentsClick = jest.fn();
collection.onNewQueryClick = jest.fn(); collection.onNewQueryClick = jest.fn();

View File

@@ -79,6 +79,14 @@ function openCollectionTab(
break; break;
} }
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
}
if ( if (
action.tabKind === ActionContracts.TabKind.TableEntities || action.tabKind === ActionContracts.TabKind.TableEntities ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] (<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]

View File

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

View File

@@ -127,13 +127,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.partitionKey.extend({ rateLimit: 100 }); this.partitionKey.extend({ rateLimit: 100 });
this.partitionKeyPattern = ko.pureComputed(() => { this.partitionKeyPattern = ko.pureComputed(() => {
if (this.container && this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
return "^/[^/]*"; return "^/[^/]*";
} }
return ".*"; return ".*";
}); });
this.partitionKeyTitle = ko.pureComputed(() => { this.partitionKeyTitle = ko.pureComputed(() => {
if (this.container && this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
return "May not use composite partition key"; return "May not use composite partition key";
} }
return ""; return "";
@@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (currentCollections >= maxCollections) { if (currentCollections >= maxCollections) {
let typeOfContainer = "collection"; let typeOfContainer = "collection";
if (this.container.isPreferredApiGraph() || this.container.isPreferredApiTable()) { if (userContext.apiType === "Gremlin" || userContext.apiType === "Tables") {
typeOfContainer = "container"; typeOfContainer = "container";
} }
@@ -368,7 +368,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return "e.g., address.zipCode"; return "e.g., address.zipCode";
} }
if (this.container && !!this.container.isPreferredApiGraph()) { if (userContext.apiType === "Gremlin") {
return "e.g., /address"; return "e.g., /address";
} }
@@ -384,21 +384,15 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.uniqueKeysVisible = ko.pureComputed<boolean>(() => { this.uniqueKeysVisible = ko.pureComputed<boolean>(() => {
if ( if (userContext.apiType === "SQL") {
this.container == null || return true;
!!this.container.isPreferredApiMongoDB() ||
!!this.container.isPreferredApiTable() ||
!!this.container.isPreferredApiCassandra() ||
!!this.container.isPreferredApiGraph()
) {
return false;
} }
return true; return false;
}); });
this.partitionKeyVisible = ko.computed<boolean>(() => { this.partitionKeyVisible = ko.computed<boolean>(() => {
if (this.container == null || !!this.container.isPreferredApiTable()) { if (this.container == null || userContext.apiType === "Tables") {
return false; return false;
} }
@@ -599,7 +593,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true; return true;
} }
if (this.container.isPreferredApiCassandra() && this.container.hasStorageAnalyticsAfecFeature()) { if (userContext.apiType === "Cassandra" && this.container.hasStorageAnalyticsAfecFeature()) {
return true; return true;
} }
@@ -763,7 +757,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return; return;
} }
if (!!this.container.isPreferredApiTable()) { if (userContext.apiType === "Tables") {
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk' // Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
this.databaseId(SharedConstants.CollectionCreation.TablesAPIDefaultDatabase); this.databaseId(SharedConstants.CollectionCreation.TablesAPIDefaultDatabase);
this.partitionKey("/'$pk'"); this.partitionKey("/'$pk'");
@@ -923,8 +917,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseId(""); this.databaseId("");
this.partitionKey(""); this.partitionKey("");
this.throughputSpendAck(false); this.throughputSpendAck(false);
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); if (!this.container.isServerlessEnabled()) {
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
}
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
@@ -958,7 +954,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
public isNonTableApi = (): boolean => { public isNonTableApi = (): boolean => {
return !this.container.isPreferredApiTable(); return userContext.apiType !== "Tables";
}; };
public isUnlimitedStorageSelected = (): boolean => { public isUnlimitedStorageSelected = (): boolean => {
@@ -1011,7 +1007,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false; return false;
} }
if (this.container.isPreferredApiGraph() && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) { if (userContext.apiType === "Gremlin" && (this.partitionKey() === "/id" || this.partitionKey() === "/label")) {
this.formErrors("/id and /label as partition keys are not allowed for graph."); this.formErrors("/id and /label as partition keys are not allowed for graph.");
return false; return false;
} }
@@ -1032,7 +1028,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
private _setFocus() { private _setFocus() {
// Autofocus is enabled on AddCollectionPane based on the preferred API // Autofocus is enabled on AddCollectionPane based on the preferred API
if (this.container.isPreferredApiTable()) { if (userContext.apiType === "Tables") {
const focusTableId = document.getElementById("containerId"); const focusTableId = document.getElementById("containerId");
focusTableId && focusTableId.focus(); focusTableId && focusTableId.focus();
return; return;

View File

@@ -62,21 +62,21 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault()); this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.databaseIdLabel = ko.computed<string>(() => this.databaseIdLabel = ko.computed<string>(() =>
this.container.isPreferredApiCassandra() ? "Keyspace id" : "Database id" userContext.apiType === "Cassandra" ? "Keyspace id" : "Database id"
); );
this.databaseIdPlaceHolder = ko.computed<string>(() => this.databaseIdPlaceHolder = ko.computed<string>(() =>
this.container.isPreferredApiCassandra() ? "Type a new keyspace id" : "Type a new database id" userContext.apiType === "Cassandra" ? "Type a new keyspace id" : "Type a new database id"
); );
this.databaseIdTooltipText = ko.computed<string>(() => { this.databaseIdTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = this.container.isPreferredApiCassandra(); const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${ return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${
isCassandraAccount ? "tables" : "collections" isCassandraAccount ? "tables" : "collections"
}`; }`;
}); });
this.databaseLevelThroughputTooltipText = ko.computed<string>(() => { this.databaseLevelThroughputTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = this.container.isPreferredApiCassandra(); const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database"; const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections"; const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;

View File

@@ -4,13 +4,13 @@ import React from "react";
import { QueriesClient } from "../../../Common/QueriesClient"; import { QueriesClient } from "../../../Common/QueriesClient";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { BrowseQueriesPanel } from "./index"; import { BrowseQueriesPane } from "./BrowseQueriesPane";
describe("Browse queries panel", () => { describe("Browse queries panel", () => {
const fakeExplorer = {} as Explorer; const fakeExplorer = {} as Explorer;
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true); fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
const fakeClientQuery = {} as QueriesClient; const fakeClientQuery = {} as QueriesClient;
const fakeQueryData = {} as Query[]; const fakeQueryData = [] as Query[];
fakeClientQuery.getQueries = async () => fakeQueryData; fakeClientQuery.getQueries = async () => fakeQueryData;
fakeExplorer.queriesClient = fakeClientQuery; fakeExplorer.queriesClient = fakeClientQuery;
const props = { const props = {
@@ -19,12 +19,12 @@ describe("Browse queries panel", () => {
}; };
it("Should render Default properly", () => { it("Should render Default properly", () => {
const wrapper = mount(<BrowseQueriesPanel {...props} />); const wrapper = mount(<BrowseQueriesPane {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("Should show empty view when query is empty []", () => { it("Should show empty view when query is empty []", () => {
const wrapper = mount(<BrowseQueriesPanel {...props} />); const wrapper = mount(<BrowseQueriesPane {...props} />);
expect(wrapper.exists("#emptyQueryBanner")).toBe(true); expect(wrapper.exists("#emptyQueryBanner")).toBe(true);
}); });
}); });

View File

@@ -13,15 +13,15 @@ import {
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab"; import QueryTab from "../../Tabs/QueryTab";
interface BrowseQueriesPanelProps { interface BrowseQueriesPaneProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
} }
export const BrowseQueriesPanel: FunctionComponent<BrowseQueriesPanelProps> = ({ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
explorer, explorer,
closePanel, closePanel,
}: BrowseQueriesPanelProps): JSX.Element => { }: BrowseQueriesPaneProps): JSX.Element => {
const loadSavedQuery = (savedQuery: Query): void => { const loadSavedQuery = (savedQuery: Query): void => {
const selectedCollection: Collection = explorer && explorer.findSelectedCollection(); const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
if (!selectedCollection) { if (!selectedCollection) {

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Browse queries panel Should render Default properly 1`] = ` exports[`Browse queries panel Should render Default properly 1`] = `
<BrowseQueriesPanel <BrowseQueriesPane
closePanel={[Function]} closePanel={[Function]}
explorer={ explorer={
Object { Object {
@@ -54,5 +54,5 @@ exports[`Browse queries panel Should render Default properly 1`] = `
</QueriesGridComponent> </QueriesGridComponent>
</div> </div>
</div> </div>
</BrowseQueriesPanel> </BrowseQueriesPane>
`; `;

View File

@@ -11,7 +11,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPane/CopyNotebookPaneComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
interface Location { interface Location {

View File

@@ -0,0 +1,156 @@
import { IDropdownOption } from "office-ui-fabric-react";
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
import { HttpStatusCodes } from "../../../Common/Constants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export interface CopyNotebookPanelProps {
name: string;
content: string;
container: Explorer;
junoClient: JunoClient;
gitHubOAuthService: GitHubOAuthService;
closePanel: () => void;
}
export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
name,
content,
container,
junoClient,
gitHubOAuthService,
closePanel,
}: CopyNotebookPanelProps) => {
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>("");
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
const [selectedLocation, setSelectedLocation] = useState<Location>();
useEffect(() => {
open();
}, []);
const open = async (): Promise<void> => {
if (gitHubOAuthService.isLoggedIn()) {
const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
}
if (response.data?.length > 0) {
setPinnedRepos(response.data);
}
}
};
const submit = async (): Promise<void> => {
let destination: string = selectedLocation?.type;
let clearMessage: () => void;
setIsExecuting(true);
try {
if (!selectedLocation) {
throw new Error(`No location selected`);
}
if (selectedLocation.type === "GitHub") {
destination = `${destination} - ${GitHubUtils.toRepoFullName(
selectedLocation.owner,
selectedLocation.repo
)} - ${selectedLocation.branch}`;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
const notebookContentItem = await copyNotebook(selectedLocation);
if (!notebookContentItem) {
throw new Error(`Failed to upload ${name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
closePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(`Failed to copy ${name} to ${destination}`);
setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally {
clearMessage && clearMessage();
setIsExecuting(false);
}
};
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem;
switch (location.type) {
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
break;
case "GitHub":
parent = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
type: NotebookContentItemType.Directory,
};
break;
default:
throw new Error(`Unsupported location type ${location.type}`);
}
return container.uploadFile(name, content, parent);
};
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
setSelectedLocation(option?.data);
};
const genericPaneProps: GenericRightPaneProps = {
container,
formError,
formErrorDetail,
id: "copynotebookpane",
isExecuting: isExecuting,
title: "Copy notebook",
submitButtonText: "OK",
onClose: closePanel,
onSubmit: () => submit(),
};
const copyNotebookPaneProps: CopyNotebookPaneProps = {
name,
pinnedRepos,
onDropDownChange: onDropDownChange,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent>
);
};

View File

@@ -1,18 +1,18 @@
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as React from "react";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { import {
Stack,
Label,
Text,
Dropdown, Dropdown,
IDropdownProps,
IDropdownOption, IDropdownOption,
SelectableOptionMenuItemType, IDropdownProps,
IRenderFunction, IRenderFunction,
ISelectableOption, ISelectableOption,
Label,
SelectableOptionMenuItemType,
Stack,
Text,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
interface Location { interface Location {
type: "MyNotebooks" | "GitHub"; type: "MyNotebooks" | "GitHub";
@@ -26,46 +26,25 @@ interface Location {
export interface CopyNotebookPaneProps { export interface CopyNotebookPaneProps {
name: string; name: string;
pinnedRepos: IPinnedRepo[]; pinnedRepos: IPinnedRepo[];
onDropDownChange: (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => void; onDropDownChange: (_: FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
} }
export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneProps> { export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps> = ({
private static readonly BranchNameWhiteSpace = " "; name,
pinnedRepos,
onDropDownChange,
}: CopyNotebookPaneProps) => {
const BranchNameWhiteSpace = " ";
public render(): JSX.Element { const onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
const dropDownProps: IDropdownProps = {
label: "Location",
ariaLabel: "Location",
placeholder: "Select an option",
onRenderTitle: this.onRenderDropDownTitle,
onRenderOption: this.onRenderDropDownOption,
options: this.getDropDownOptions(),
onChange: this.props.onDropDownChange,
};
return (
<div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
<Label htmlFor="notebookName">Name</Label>
<Text id="notebookName">{this.props.name}</Text>
</Stack.Item>
<Dropdown {...dropDownProps} />
</Stack>
</div>
);
}
private onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
return <span>{options.length && options[0].title}</span>; return <span>{options.length && options[0].title}</span>;
}; };
private onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => { const onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>; return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
}; };
private getDropDownOptions = (): IDropdownOption[] => { const getDropDownOptions = (): IDropdownOption[] => {
const options: IDropdownOption[] = []; const options: IDropdownOption[] = [];
options.push({ options.push({
@@ -77,7 +56,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
} as Location, } as Location,
}); });
if (this.props.pinnedRepos && this.props.pinnedRepos.length > 0) { if (pinnedRepos && pinnedRepos.length > 0) {
options.push({ options.push({
key: "GitHub-Header-Divider", key: "GitHub-Header-Divider",
text: undefined, text: undefined,
@@ -90,7 +69,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
itemType: SelectableOptionMenuItemType.Header, itemType: SelectableOptionMenuItemType.Header,
}); });
this.props.pinnedRepos.forEach((pinnedRepo) => { pinnedRepos.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
options.push({ options.push({
key: `GitHub-Repo-${repoFullName}`, key: `GitHub-Repo-${repoFullName}`,
@@ -101,7 +80,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
pinnedRepo.branches.forEach((branch) => pinnedRepo.branches.forEach((branch) =>
options.push({ options.push({
key: `GitHub-Repo-${repoFullName}-${branch.name}`, key: `GitHub-Repo-${repoFullName}-${branch.name}`,
text: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`, text: `${BranchNameWhiteSpace}${branch.name}`,
title: `${repoFullName} - ${branch.name}`, title: `${repoFullName} - ${branch.name}`,
data: { data: {
type: "GitHub", type: "GitHub",
@@ -116,4 +95,26 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
return options; return options;
}; };
} const dropDownProps: IDropdownProps = {
label: "Location",
ariaLabel: "Location",
placeholder: "Select an option",
onRenderTitle: onRenderDropDownTitle,
onRenderOption: onRenderDropDownOption,
options: getDropDownOptions(),
onChange: onDropDownChange,
};
return (
<div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
<Label htmlFor="notebookName">Name</Label>
<Text id="notebookName">{name}</Text>
</Stack.Item>
<Dropdown {...dropDownProps} />
</Stack>
</div>
);
};

View File

@@ -3,7 +3,6 @@ jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme"; import { mount, ReactWrapper, shallow } from "enzyme";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import { DeleteCollectionConfirmationPanel } from ".";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection"; import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback"; import DeleteFeedback from "../../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels"; import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
@@ -13,6 +12,7 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
describe("Delete Collection Confirmation Pane", () => { describe("Delete Collection Confirmation Pane", () => {
describe("Explorer.isLastCollection()", () => { describe("Explorer.isLastCollection()", () => {
@@ -65,7 +65,7 @@ describe("Delete Collection Confirmation Pane", () => {
closePanel: (): void => undefined, closePanel: (): void => undefined,
collectionName: "container", collectionName: "container",
}; };
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />); const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
props.explorer.isLastCollection = () => true; props.explorer.isLastCollection = () => true;
@@ -119,7 +119,7 @@ describe("Delete Collection Confirmation Pane", () => {
closePanel: (): void => undefined, closePanel: (): void => undefined,
collectionName: "container", collectionName: "container",
}; };
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />); wrapper = mount(<DeleteCollectionConfirmationPane {...props} />);
}); });
it("should call delete collection", () => { it("should call delete collection", () => {

View File

@@ -11,18 +11,21 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import {
export interface DeleteCollectionConfirmationPanelProps { GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface DeleteCollectionConfirmationPaneProps {
explorer: Explorer; explorer: Explorer;
collectionName: string; collectionName: string;
closePanel: () => void; closePanel: () => void;
} }
export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollectionConfirmationPanelProps> = ({ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
explorer, explorer,
closePanel, closePanel,
collectionName, collectionName,
}: DeleteCollectionConfirmationPanelProps) => { }: DeleteCollectionConfirmationPaneProps) => {
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>(""); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
const [inputCollectionName, setInputCollectionName] = useState<string>(""); const [inputCollectionName, setInputCollectionName] = useState<string>("");
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
@@ -97,7 +100,7 @@ export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollecti
); );
} }
}; };
const genericPaneProps: RightPaneFormProps = { const genericPaneProps: GenericRightPaneProps = {
container: explorer, container: explorer,
formError: formError, formError: formError,
formErrorDetail: formError, formErrorDetail: formError,
@@ -109,7 +112,7 @@ export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollecti
onSubmit: submit, onSubmit: submit,
}; };
return ( return (
<RightPaneForm {...genericPaneProps}> <GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
@@ -147,6 +150,6 @@ export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollecti
)} )}
</div> </div>
</div> </div>
</RightPaneForm> </GenericRightPaneComponent>
); );
}; };

View File

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

View File

@@ -4,7 +4,10 @@ import React, { FunctionComponent, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure"; import StoredProcedure from "../../Tree/StoredProcedure";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent"; import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { InputParameter } from "./InputParameter"; import { InputParameter } from "./InputParameter";
interface ExecuteSprocParamsPaneProps { interface ExecuteSprocParamsPaneProps {
@@ -23,7 +26,7 @@ interface UnwrappedExecuteSprocParam {
text: string; text: string;
} }
export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
explorer, explorer,
storedProcedure, storedProcedure,
closePanel, closePanel,
@@ -79,8 +82,15 @@ export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPanePr
return; return;
} }
setLoadingTrue(); setLoadingTrue();
const sprocParams = wrappedSprocParams && wrappedSprocParams.map((sprocParam) => sprocParam.text); const sprocParams =
storedProcedure.execute(sprocParams, partitionValue); wrappedSprocParams &&
wrappedSprocParams.map((sprocParam) => {
if (sprocParam.key === "custom") {
return JSON.parse(sprocParam.text);
}
return sprocParam.text;
});
storedProcedure.execute(sprocParams, partitionKey === "custom" ? JSON.parse(partitionValue) : partitionValue);
setLoadingFalse(); setLoadingFalse();
closePanel(); closePanel();
}; };

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Excute Sproc Param Pane should render Default properly 1`] = ` exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<ExecuteSprocParamsPanel <ExecuteSprocParamsPane
closePanel={[Function]} closePanel={[Function]}
explorer={Object {}} explorer={Object {}}
storedProcedure={Object {}} storedProcedure={Object {}}
@@ -8176,5 +8176,5 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</GenericRightPaneComponent> </GenericRightPaneComponent>
</ExecuteSprocParamsPanel> </ExecuteSprocParamsPane>
`; `;

View File

@@ -19,10 +19,6 @@ export interface GenericRightPaneProps {
children?: ReactNode; children?: ReactNode;
} }
export interface GenericRightPaneState {
panelHeight: number;
}
export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({ export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({
container, container,
formError, formError,

View File

@@ -1,60 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" data-bind="attr: { id: id }">
<!-- New Vertex form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- New Vertex header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2">New Vertex</span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
tabindex="0"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- New Vertex header - End -->
<!-- New Vertex errors - Start -->
<div
aria-live="assertive"
class="warningErrorContainer"
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, event: { keypress: onMoreDetailsKeyPress }"
tabindex="0"
>
More details
</a>
</span>
</div>
</div>
<!-- New Vertex errors - End -->
<!-- New Vertex inputs - Start -->
<div class="paneMainContent">
<new-vertex-form
class="newvertexContainer"
params="{ newVertexData: tempVertexData, firstFieldHasFocus: firstFieldHasFocus, partitionKeyProperty: partitionKeyProperty }"
></new-vertex-form>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
<!-- New Vertex inputs - End -->
</form>
</div>
<!-- New Vertex form - End -->
</div>
</div>

View File

@@ -1,10 +0,0 @@
@import "../../../less/Common/Constants";
.newvertexContainer {
height:100%;
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
.flex-display();
.flex-direction();
}

View File

@@ -1,7 +1,7 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { LoadQueryPanel } from "./index"; import { LoadQueryPane } from "./LoadQueryPane";
describe("Load Query Pane", () => { describe("Load Query Pane", () => {
it("should render Default properly", () => { it("should render Default properly", () => {
@@ -11,7 +11,7 @@ describe("Load Query Pane", () => {
closePanel: (): void => undefined, closePanel: (): void => undefined,
}; };
const wrapper = shallow(<LoadQueryPanel {...props} />); const wrapper = shallow(<LoadQueryPane {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@@ -3,22 +3,25 @@ import { IImageProps, Image, ImageFit, Stack, TextField } from "office-ui-fabric
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import folderIcon from "../../../../images/folder_16x16.svg"; import folderIcon from "../../../../images/folder_16x16.svg";
import { logError } from "../../../Common/Logger"; import { logError } from "../../../Common/Logger";
import { Collection } from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab"; import QueryTab from "../../Tabs/QueryTab";
import { Collection } from "..//../../Contracts/ViewModels"; import {
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent"; GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
interface LoadQueryPanelProps { interface LoadQueryPaneProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
} }
export const LoadQueryPanel: FunctionComponent<LoadQueryPanelProps> = ({ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
explorer, explorer,
closePanel, closePanel,
}: LoadQueryPanelProps): JSX.Element => { }: LoadQueryPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>(""); const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");

View File

@@ -1,65 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { KeyCodes } from "../../Common/Constants";
import Explorer from "../Explorer";
export default class NewVertexPane extends ContextualPaneBase {
public container: Explorer;
public visible: ko.Observable<boolean>;
public formErrors: ko.Observable<string>;
public formErrorsDetails: ko.Observable<string>;
// Graph style stuff
public tempVertexData: ko.Observable<ViewModels.NewVertexData>; // vertex data being edited
private onSubmitCreateCallback: (newVertexData: ViewModels.NewVertexData) => void;
private partitionKeyProperty: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.tempVertexData = ko.observable<ViewModels.NewVertexData>(null);
this.partitionKeyProperty = ko.observable(null);
this.resetData();
}
public submit() {
// Commit edited changes
if (this.onSubmitCreateCallback != null) {
this.onSubmitCreateCallback(this.tempVertexData());
}
// this.close();
}
public resetData() {
super.resetData();
this.onSubmitCreateCallback = null;
this.tempVertexData({
label: "",
properties: <ViewModels.InputProperty[]>[],
});
this.partitionKeyProperty(null);
}
public subscribeOnSubmitCreate(callback: (newVertexData: ViewModels.NewVertexData) => void): void {
this.onSubmitCreateCallback = callback;
}
public setPartitionKeyProperty(pKeyProp: string): void {
this.partitionKeyProperty(pKeyProp);
}
public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
this.showErrorDetails();
return false;
}
return true;
};
public buildString = (prefix: string, index: number): string => {
return `${prefix}${index}`;
};
}

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