resolve master branch conflict

This commit is contained in:
hardiknai-techm
2021-04-24 11:10:38 +05:30
191 changed files with 12929 additions and 7043 deletions

View File

@@ -89,8 +89,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
@@ -120,19 +119,18 @@ 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
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/SplashScreen/SplashScreen.test.ts src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts src/Explorer/Tables/DataTable/CacheBase.ts

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"],
};

5674
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,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",
@@ -80,6 +81,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",
@@ -113,15 +115,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",
@@ -133,7 +132,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",
@@ -148,16 +146,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",
@@ -165,16 +165,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

@@ -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

@@ -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>;
@@ -366,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);
}); });
@@ -24,59 +20,10 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true); expect(ko.components.isRegistered("json-editor")).toBe(true);
}); });
it("should register documents-tab component", () => {
expect(ko.components.isRegistered("documents-tab")).toBe(true);
});
it("should register stored-procedure-tab component", () => {
expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true);
});
it("should register trigger-tab component", () => {
expect(ko.components.isRegistered("trigger-tab")).toBe(true);
});
it("should register user-defined-function-tab component", () => {
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
});
it("should register settings-tab-v2 component", () => {
expect(ko.components.isRegistered("database-settings-tab-v2")).toBe(true);
expect(ko.components.isRegistered("collection-settings-tab-v2")).toBe(true);
});
it("should register query-tab component", () => {
expect(ko.components.isRegistered("query-tab")).toBe(true);
});
it("should register tables-query-tab component", () => {
expect(ko.components.isRegistered("tables-query-tab")).toBe(true);
});
it("should register graph-tab component", () => {
expect(ko.components.isRegistered("graph-tab")).toBe(true);
});
it("should register notebookv2-tab component", () => {
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
});
it("should register terminal-tab component", () => {
expect(ko.components.isRegistered("terminal-tab")).toBe(true);
});
it("should register mongo-shell-tab component", () => {
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
});
it("should registeradd-collection-pane component", () => { it("should registeradd-collection-pane component", () => {
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 +32,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,26 +7,9 @@ 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 DocumentsTab from "./Tabs/DocumentsTab";
import GalleryTab from "./Tabs/GalleryTab";
import GraphTab from "./Tabs/GraphTab";
import MongoShellTab from "./Tabs/MongoShellTab";
import NotebookTabV2 from "./Tabs/NotebookV2Tab";
import NotebookViewerTab from "./Tabs/NotebookViewerTab";
import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TabsManagerTemplate from "./Tabs/TabsManager.html";
import TerminalTab from "./Tabs/TerminalTab";
import TriggerTab from "./Tabs/TriggerTab";
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("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,37 +17,14 @@ 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
[
DocumentsTab,
StoredProcedureTab,
TriggerTab,
UserDefinedFunctionTab,
SettingsTabV2,
QueryTab,
QueryTablesTab,
GraphTab,
MongoShellTab,
ConflictsTab,
NotebookTabV2,
TerminalTab,
GalleryTab,
NotebookViewerTab,
DatabaseSettingsTabV2,
].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

@@ -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,43 +50,52 @@ 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,
textFieldProps,
linkProps,
progressIndicatorProps,
primaryButtonText,
secondaryButtonText,
onPrimaryButtonClick,
onSecondaryButtonClick,
primaryButtonDisabled,
type,
showCloseButton,
onDismiss,
}: DialogProps) => {
const dialogProps: IDialogProps = { const dialogProps: IDialogProps = {
hidden: !this.props.visible, hidden: !visible,
dialogContentProps: { dialogContentProps: {
type: this.props.type || DialogType.normal, type: type || DialogType.normal,
title: this.props.title, title,
subText: this.props.subText, subText,
styles: { styles: {
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT }, title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }, subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE },
}, },
showCloseButton: this.props.showCloseButton || false, showCloseButton: showCloseButton || false,
onDismiss: this.props.onDismiss, onDismiss,
}, },
modalProps: { isBlocking: this.props.isModal, isDarkOverlay: false }, modalProps: { isBlocking: isModal, isDarkOverlay: false },
minWidth: DIALOG_MIN_WIDTH, minWidth: DIALOG_MIN_WIDTH,
maxWidth: DIALOG_MAX_WIDTH, maxWidth: DIALOG_MAX_WIDTH,
}; };
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps;
const progressIndicatorProps: IProgressIndicatorProps = this.props.progressIndicatorProps;
const primaryButtonProps: IButtonProps = { const primaryButtonProps: IButtonProps = {
text: this.props.primaryButtonText, text: primaryButtonText,
disabled: this.props.primaryButtonDisabled || false, disabled: primaryButtonDisabled || false,
onClick: this.props.onPrimaryButtonClick, onClick: onPrimaryButtonClick,
}; };
const secondaryButtonProps: IButtonProps = const secondaryButtonProps: IButtonProps =
this.props.secondaryButtonText && this.props.onSecondaryButtonClick secondaryButtonText && onSecondaryButtonClick
? { ? {
text: this.props.secondaryButtonText, text: secondaryButtonText,
onClick: this.props.onSecondaryButtonClick, onClick: onSecondaryButtonClick,
} }
: undefined; : undefined;
@@ -106,5 +115,4 @@ export class Dialog extends React.Component<DialogProps> {
</DialogFooter> </DialogFooter>
</FluentDialog> </FluentDialog>
); );
} };
}

View File

@@ -350,11 +350,11 @@ exports[`test render renders with filters 1`] = `
} }
> >
<div <div
className="ms-ScrollablePane root-72" className="ms-ScrollablePane root-40"
data-is-scrollable="true" data-is-scrollable="true"
> >
<div <div
className="stickyAbove-74" className="stickyAbove-42"
style={ style={
Object { Object {
"height": 0, "height": 0,
@@ -365,7 +365,7 @@ exports[`test render renders with filters 1`] = `
} }
/> />
<div <div
className="ms-ScrollablePane--contentContainer contentContainer-73" className="ms-ScrollablePane--contentContainer contentContainer-41"
data-is-scrollable={true} data-is-scrollable={true}
> >
<Sticky <Sticky
@@ -691,18 +691,18 @@ exports[`test render renders with filters 1`] = `
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField directoryListFilterTextBox root-78" className="ms-TextField directoryListFilterTextBox root-46"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-79" className="ms-TextField-fieldGroup fieldGroup-47"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Directory filter text box" aria-label="Directory filter text box"
className="ms-TextField-field field-80" className="ms-TextField-field field-48"
id="TextField0" id="TextField0"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -1900,7 +1900,7 @@ exports[`test render renders with filters 1`] = `
> >
<button <button
aria-disabled={true} aria-disabled={true}
className="ms-Button ms-Button--default is-disabled directoryListButton root-89" className="ms-Button ms-Button--default is-disabled directoryListButton root-57"
data-is-focusable={false} data-is-focusable={false}
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
@@ -1912,7 +1912,7 @@ exports[`test render renders with filters 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-90" className="ms-Button-flexContainer flexContainer-58"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<div <div
@@ -1943,7 +1943,7 @@ exports[`test render renders with filters 1`] = `
</List> </List>
</div> </div>
<div <div
className="stickyBelow-75" className="stickyBelow-43"
style={ style={
Object { Object {
"bottom": "0px", "bottom": "0px",
@@ -1954,7 +1954,7 @@ exports[`test render renders with filters 1`] = `
} }
> >
<div <div
className="stickyBelowItems-76" className="stickyBelowItems-44"
/> />
</div> </div>
</div> </div>

View File

@@ -321,7 +321,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public getPartitionKeyVisible = (): boolean => { public getPartitionKeyVisible = (): boolean => {
if ( if (
userContext.apiType === "Cassandra" || 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

@@ -1,6 +1,6 @@
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
import React from "react"; import React from "react";
import { ThroughputInput } from "."; import { ThroughputInput } from "./ThroughputInput";
const props = { const props = {
isDatabase: false, isDatabase: false,
showFreeTierExceedThroughputTooltip: true, showFreeTierExceedThroughputTooltip: true,

View File

@@ -11,7 +11,7 @@ import {
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { Tooltip } from "../../../Common/Tooltip"; import { Tooltip } from "../../../Common/Tooltip/Tooltip";
import * as SharedConstants from "../../../Shared/Constants"; import * as SharedConstants from "../../../Shared/Constants";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";

View File

@@ -15,7 +15,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<div <div
className="ms-Stack css-72" className="ms-Stack css-40"
> >
<span <span
className="mandatoryStar" className="mandatoryStar"
@@ -33,7 +33,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
variant="small" variant="small"
> >
<span <span
className="css-73" className="css-41"
style={ style={
Object { Object {
"lineHeight": "20px", "lineHeight": "20px",
@@ -1377,7 +1377,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<button <button
aria-label="Info" aria-label="Info"
className="ms-Button ms-Button--icon root-74" className="ms-Button ms-Button--icon root-42"
data-is-focusable={true} data-is-focusable={true}
id="iconButton1" id="iconButton1"
onClick={[Function]} onClick={[Function]}
@@ -1389,16 +1389,16 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-75" className="ms-Button-flexContainer flexContainer-43"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<Component <Component
className="ms-Button-icon icon-77" className="ms-Button-icon icon-45"
iconName="Info" iconName="Info"
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Icon root-37 css-82 ms-Button-icon icon-77" className="ms-Icon root-37 css-50 ms-Button-icon icon-45"
data-icon-name="Info" data-icon-name="Info"
role="presentation" role="presentation"
style={ style={
@@ -1425,7 +1425,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</Stack> </Stack>
<Stack> <Stack>
<div <div
className="ms-Stack css-83" className="ms-Stack css-51"
> >
<StyledChoiceGroupBase <StyledChoiceGroupBase
aria-label="mode" aria-label="mode"
@@ -1741,7 +1741,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="" className=""
> >
<div <div
className="ms-ChoiceFieldGroup root-84" className="ms-ChoiceFieldGroup root-52"
role="radiogroup" role="radiogroup"
> >
<div <div
@@ -2051,14 +2051,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
} }
> >
<div <div
className="ms-ChoiceField root-85" className="ms-ChoiceField root-53"
> >
<div <div
className="ms-ChoiceField-wrapper" className="ms-ChoiceField-wrapper"
> >
<input <input
checked={true} checked={true}
className="ms-ChoiceField-input input-86" className="ms-ChoiceField-input input-54"
id="ChoiceGroup6-true" id="ChoiceGroup6-true"
name="ChoiceGroup6" name="ChoiceGroup6"
onBlur={[Function]} onBlur={[Function]}
@@ -2067,7 +2067,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
type="radio" type="radio"
/> />
<label <label
className="ms-ChoiceField-field is-checked field-87" className="ms-ChoiceField-field is-checked field-55"
htmlFor="ChoiceGroup6-true" htmlFor="ChoiceGroup6-true"
> >
<span <span
@@ -2385,14 +2385,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
} }
> >
<div <div
className="ms-ChoiceField root-85" className="ms-ChoiceField root-53"
> >
<div <div
className="ms-ChoiceField-wrapper" className="ms-ChoiceField-wrapper"
> >
<input <input
checked={false} checked={false}
className="ms-ChoiceField-input input-86" className="ms-ChoiceField-input input-54"
id="ChoiceGroup6-false" id="ChoiceGroup6-false"
name="ChoiceGroup6" name="ChoiceGroup6"
onBlur={[Function]} onBlur={[Function]}
@@ -2401,7 +2401,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
type="radio" type="radio"
/> />
<label <label
className="ms-ChoiceField-field field-92" className="ms-ChoiceField-field field-60"
htmlFor="ChoiceGroup6-false" htmlFor="ChoiceGroup6-false"
> >
<span <span
@@ -2426,7 +2426,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="throughputInputSpacing" className="throughputInputSpacing"
> >
<div <div
className="ms-Stack throughputInputSpacing css-83" className="ms-Stack throughputInputSpacing css-51"
> >
<Text <Text
data-testid="ruDescription" data-testid="ruDescription"
@@ -2434,7 +2434,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
variant="small" variant="small"
> >
<span <span
className="css-73" className="css-41"
data-testid="ruDescription" data-testid="ruDescription"
> >
Provision maximum RU/s required by this resource. Estimate your required RU/s with  Provision maximum RU/s required by this resource. Estimate your required RU/s with 
@@ -2723,7 +2723,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
} }
> >
<a <a
className="ms-Link root-95" className="ms-Link root-63"
data-testid="ruDescription" data-testid="ruDescription"
href="https://cosmos.azure.com/capacitycalculator/" href="https://cosmos.azure.com/capacitycalculator/"
onClick={[Function]} onClick={[Function]}
@@ -2741,7 +2741,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
key=".0:$.1" key=".0:$.1"
> >
<div <div
className="ms-Stack css-72" className="ms-Stack css-40"
> >
<Text <Text
data-testid="maxRUDescription" data-testid="maxRUDescription"
@@ -2754,7 +2754,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
variant="small" variant="small"
> >
<span <span
className="css-73" className="css-41"
data-testid="maxRUDescription" data-testid="maxRUDescription"
style={ style={
Object { Object {
@@ -4101,7 +4101,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<button <button
aria-label="Info" aria-label="Info"
className="ms-Button ms-Button--icon root-74" className="ms-Button ms-Button--icon root-42"
data-is-focusable={true} data-is-focusable={true}
id="iconButton9" id="iconButton9"
onClick={[Function]} onClick={[Function]}
@@ -4113,16 +4113,16 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-75" className="ms-Button-flexContainer flexContainer-43"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<Component <Component
className="ms-Button-icon icon-77" className="ms-Button-icon icon-45"
iconName="Info" iconName="Info"
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Icon root-37 css-82 ms-Button-icon icon-77" className="ms-Icon root-37 css-50 ms-Button-icon icon-45"
data-icon-name="Info" data-icon-name="Info"
role="presentation" role="presentation"
style={ style={
@@ -4456,17 +4456,17 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
value="4000" value="4000"
> >
<div <div
className="ms-TextField is-required root-97" className="ms-TextField is-required root-65"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-98" className="ms-TextField-fieldGroup fieldGroup-66"
> >
<input <input
aria-invalid={false} aria-invalid={false}
className="ms-TextField-field field-99" className="ms-TextField-field field-67"
id="TextField14" id="TextField14"
min={4000} min={4000}
onBlur={[Function]} onBlur={[Function]}
@@ -4488,7 +4488,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
variant="small" variant="small"
> >
<span <span
className="css-73" className="css-41"
> >
Your Your
container container

View File

@@ -15,7 +15,6 @@ describe("ContainerSampleGenerator", () => {
const explorerStub = {} as Explorer; const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]); explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false); explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiTable = 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();

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);
} }
/** /**

View File

@@ -36,43 +36,47 @@ 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";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { DialogProps, TextFieldProps } from "./Controls/Dialog"; import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab as GalleryTabKind } 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 type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
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 { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel"; import { AddDatabasePanel } from "./Panes/AddDatabasePanelF/AddDatabasePanelF";
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 type { GalleryTabOptions } from "./Tabs/GalleryTab";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase"; import QueryTablesTab from "./Tabs/QueryTablesTab";
import { TabsManager } from "./Tabs/TabsManager"; import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
@@ -122,12 +126,6 @@ export default class Explorer {
* 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 === "Tables"
* */
public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
@@ -170,28 +168,21 @@ export default class Explorer {
// Tabs // Tabs
public isTabsContentExpanded: ko.Observable<boolean>; public isTabsContentExpanded: ko.Observable<boolean>;
public galleryTab: any;
public notebookViewerTab: any;
public tabsManager: TabsManager; public tabsManager: TabsManager;
// Contextual panes // Contextual panes
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>;
@@ -213,7 +204,7 @@ export default class Explorer {
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>; public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Observable<boolean>; public isSynapseLinkUpdating: ko.Observable<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>; public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any; // This is dynamically loaded public notebookManager?: NotebookManager;
public openDialog: ExplorerParams["openDialog"]; public openDialog: ExplorerParams["openDialog"];
public closeDialog: ExplorerParams["closeDialog"]; public closeDialog: ExplorerParams["closeDialog"];
@@ -289,7 +280,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,
@@ -342,7 +332,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);
@@ -406,11 +395,6 @@ export default class Explorer {
}); });
}); });
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;
@@ -503,7 +487,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),
@@ -517,13 +501,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),
@@ -531,13 +508,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),
@@ -552,13 +522,6 @@ 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) => { this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) { if (tabs.length === 0) {
@@ -571,12 +534,9 @@ export default class Explorer {
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);
@@ -645,7 +605,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;
@@ -660,7 +619,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;
@@ -679,10 +637,10 @@ export default class Explorer {
this.isNotebookEnabled = ko.observable(false); this.isNotebookEnabled = ko.observable(false);
this.isNotebookEnabled.subscribe(async () => { this.isNotebookEnabled.subscribe(async () => {
if (!this.notebookManager) { if (!this.notebookManager) {
const notebookManagerModule = await import( const NotebookManager = await (
/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager" await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
); ).default;
this.notebookManager = new notebookManagerModule.default(); this.notebookManager = new NotebookManager();
this.notebookManager.initialize({ this.notebookManager.initialize({
container: this, container: this,
notebookBasePath: this.notebookBasePath, notebookBasePath: this.notebookBasePath,
@@ -756,8 +714,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);
@@ -775,19 +732,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);
@@ -914,10 +865,7 @@ export default class Explorer {
}, },
startKey startKey
); );
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error while refreshing databases: ${errorMessage}`);
ConsoleDataType.Error,
`Error while refreshing databases: ${errorMessage}`
);
} }
); );
@@ -1138,20 +1086,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();
} }
}; };
@@ -1477,7 +1425,11 @@ export default class Explorer {
return Promise.resolve(false); return Promise.resolve(false);
} }
public async publishNotebook(name: string, content: string | unknown, parentDomElement?: HTMLElement): Promise<void> { public async publishNotebook(
name: string,
content: NotebookPaneContent,
parentDomElement?: HTMLElement
): Promise<void> {
if (this.notebookManager) { if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement); await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
@@ -1486,11 +1438,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 {
@@ -1708,11 +1656,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();
} }
); );
@@ -1864,15 +1808,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)}`
);
}
); );
} }
@@ -1888,11 +1825,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,
}); });
@@ -1900,7 +1833,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,
{ {
@@ -1913,7 +1846,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,
{ {
@@ -1924,7 +1857,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> {
@@ -1994,86 +1927,66 @@ export default class Explorer {
} }
public async openGallery( public async openGallery(
selectedTab?: GalleryTab, selectedTab?: GalleryTabKind,
notebookUrl?: string, notebookUrl?: string,
galleryItem?: IGalleryItem, galleryItem?: IGalleryItem,
isFavorite?: boolean isFavorite?: boolean
) { ) {
let title: string = "Gallery"; const title = "Gallery";
let hashLocation: string = "gallery"; const hashLocation = "gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTabOptions: any = { const galleryTabOptions: GalleryTabOptions = {
// GalleryTabOptions
account: userContext.databaseAccount, account: userContext.databaseAccount,
container: this, container: this,
junoClient: this.notebookManager?.junoClient, junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTab.PublicGallery, selectedTab: selectedTab || GalleryTabKind.PublicGallery,
notebookUrl, notebookUrl,
galleryItem, galleryItem,
isFavorite, isFavorite,
// TabOptions
tabKind: ViewModels.CollectionTabKind.Gallery, tabKind: ViewModels.CollectionTabKind.Gallery,
title: title, title: title,
tabPath: title, tabPath: title,
documentClientUtility: null,
isActive: ko.observable(false),
hashLocation: hashLocation, hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons, onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null, onLoadStartKey: null,
}; };
const galleryTabs = this.tabsManager.getTabs( const galleryTab = this.tabsManager
ViewModels.CollectionTabKind.Gallery, .getTabs(ViewModels.CollectionTabKind.Gallery)
(tab) => tab.hashLocation() == hashLocation .find((tab) => tab.hashLocation() == hashLocation);
);
let galleryTab = galleryTabs && galleryTabs[0];
if (galleryTab) { if (galleryTab instanceof GalleryTab) {
this.tabsManager.activateTab(galleryTab); this.tabsManager.activateTab(galleryTab);
(galleryTab as any).reset(galleryTabOptions); galleryTab.reset(galleryTabOptions);
} else { } else {
if (!this.galleryTab) { this.tabsManager.activateNewTab(new GalleryTab(galleryTabOptions));
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
}
const newTab = new this.galleryTab.default(galleryTabOptions);
this.tabsManager.activateNewTab(newTab);
} }
} }
public async openNotebookViewer(notebookUrl: string) { public async openNotebookViewer(notebookUrl: string) {
const title = path.basename(notebookUrl); const title = path.basename(notebookUrl);
const hashLocation = notebookUrl; const hashLocation = notebookUrl;
const NotebookViewerTab = await (
await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab")
).default;
if (!this.notebookViewerTab) { const notebookViewerTab = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2).find((tab) => {
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab"); return tab.hashLocation() == hashLocation && tab instanceof NotebookViewerTab && tab.notebookUrl === notebookUrl;
}
const notebookViewerTabModule = this.notebookViewerTab;
let isNotebookViewerOpen = (tab: TabsBase) => {
const notebookViewerTab = tab as typeof notebookViewerTabModule.default;
return notebookViewerTab.notebookUrl === notebookUrl;
};
const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab) => {
return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab);
}); });
let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0];
if (notebookViewerTab) { if (notebookViewerTab) {
this.tabsManager.activateNewTab(notebookViewerTab); this.tabsManager.activateNewTab(notebookViewerTab);
} else { } else {
notebookViewerTab = new this.notebookViewerTab.default({ const notebookViewerTab = new NotebookViewerTab({
account: userContext.databaseAccount, account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookViewer, tabKind: ViewModels.CollectionTabKind.NotebookViewer,
node: null, node: null,
title: title, title: title,
tabPath: title, tabPath: title,
documentClientUtility: null,
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,
@@ -2134,7 +2047,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> {
@@ -2199,7 +2112,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}
@@ -2230,7 +2143,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()}
@@ -2257,15 +2170,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 {
@@ -2280,6 +2193,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

@@ -29,7 +29,6 @@ export class CommandBarComponentAdapter implements ReactAdapter {
// 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.deleteCollectionText, container.deleteCollectionText,
container.deleteDatabaseText, container.deleteDatabaseText,

View File

@@ -16,7 +16,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({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableTable" }],
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = 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,7 +60,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({
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);
@@ -117,7 +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({
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);
@@ -195,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);
@@ -226,6 +251,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
}, },
} as DatabaseAccount, } 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();
@@ -288,7 +314,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({
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);

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);
@@ -446,7 +446,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 +483,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 +509,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,7 +123,9 @@ export class NotebookClientV2 {
contents: makeContentsRecord({ contents: makeContentsRecord({
byRef: Immutable.Map<string, ContentRecord>(), byRef: Immutable.Map<string, ContentRecord>(),
}), }),
transforms: makeTransformsRecord({ transforms: userContext.features.sandboxNotebookOutputs
? undefined
: makeTransformsRecord({
displayOrder: Immutable.List([ displayOrder: Immutable.List([
"application/vnd.jupyter.widget-view+json", "application/vnd.jupyter.widget-view+json",
"application/vnd.vega.v5+json", "application/vnd.vega.v5+json",
@@ -168,10 +168,8 @@ export class NotebookClientV2 {
"application/vnd.vega.v5+json": NullTransform, "application/vnd.vega.v5+json": NullTransform,
"application/vdom.v1+json": TransformVDOM, "application/vdom.v1+json": TransformVDOM,
"application/json": Media.Json, "application/json": Media.Json,
"application/javascript": userContext.features.sandboxNotebookOutputs "application/javascript": Media.JavaScript,
? SandboxJavaScript "text/html": Media.HTML,
: Media.JavaScript,
"text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : Media.HTML,
"text/markdown": Media.Markdown, "text/markdown": Media.Markdown,
"text/latex": Media.LaTeX, "text/latex": Media.LaTeX,
"image/svg+xml": Media.SVG, "image/svg+xml": Media.SVG,
@@ -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,35 @@
* 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 type { ImmutableNotebook } from "@nteract/commutable";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import type { IContentProvider } from "@nteract/core";
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 ko from "knockout";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import React from "react";
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 { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { MemoryUsageInfo } from "../../Contracts/DataModels"; import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient"; import { GitHubClient } from "../../GitHub/GitHubClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; 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 { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane"; 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";
type NotebookPaneContent = string | ImmutableNotebook;
export type { NotebookPaneContent };
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
@@ -49,7 +54,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 +93,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);
@@ -122,14 +120,25 @@ export default class NotebookManager {
public async openPublishNotebookPane( public async openPublishNotebookPane(
name: string, name: string,
content: string | ImmutableNotebook, content: NotebookPaneContent,
parentDomElement: HTMLElement parentDomElement: HTMLElement
): Promise<void> { ): Promise<void> {
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement); await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
} }
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,8 +25,10 @@ export interface NotebookRendererProps {
*/ */
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> { class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() { componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any); loadTransform(this.props as any);
} }
}
private renderPrompt(id: string, contentRef: string): JSX.Element { private renderPrompt(id: string, contentRef: string): JSX.Element {
if (this.props.hidePrompts) { if (this.props.hidePrompts) {
@@ -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() {
if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any); 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

@@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (currentCollections >= maxCollections) { if (currentCollections >= maxCollections) {
let typeOfContainer = "collection"; let typeOfContainer = "collection";
if (userContext.apiType === "Gremlin" || this.container.isPreferredApiTable()) { if (userContext.apiType === "Gremlin" || userContext.apiType === "Tables") {
typeOfContainer = "container"; typeOfContainer = "container";
} }
@@ -392,7 +392,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
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;
} }
@@ -757,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'");
@@ -917,8 +917,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseId(""); this.databaseId("");
this.partitionKey(""); this.partitionKey("");
this.throughputSpendAck(false); this.throughputSpendAck(false);
if (!this.container.isServerlessEnabled()) {
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
}
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
@@ -952,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 => {
@@ -1026,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

@@ -26,7 +26,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getUpsellMessage } from "../../Utils/PricingUtils"; import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent"; import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";

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 { AddDatabasePanel } from "./AddDatabasePanel"; import { AddDatabasePanel } from "./AddDatabasePanelF";
const props = { const props = {
explorer: new Explorer(), explorer: new Explorer(),

View File

@@ -3,7 +3,7 @@ import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { createDatabase } from "../../../Common/dataAccess/createDatabase"; import { createDatabase } from "../../../Common/dataAccess/createDatabase";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Tooltip } from "../../../Common/Tooltip"; import { Tooltip } from "../../../Common/Tooltip/Tooltip";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { SubscriptionType } from "../../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../../Contracts/SubscriptionType";
@@ -13,9 +13,12 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils"; import * as PricingUtils from "../../../Utils/PricingUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent"; import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
export interface AddDatabasePaneProps { export interface AddDatabasePaneProps {

View File

@@ -9,6 +9,48 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [ "_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
AddCollectionPane { AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
@@ -91,21 +133,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"useIndexingForSharedThroughput": [Function], "useIndexingForSharedThroughput": [Function],
"visible": [Function], "visible": [Function],
}, },
DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
GraphStylingPane { GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -126,42 +153,6 @@ exports[`AddDatabasePane Pane should render Default properly 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",
@@ -196,68 +187,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane {
"buildString": [Function],
"container": [Circular],
"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],
@@ -319,20 +248,6 @@ exports[`AddDatabasePane Pane should render Default properly 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],
@@ -419,43 +334,49 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabaseText": [Function], "addDatabasePane": AddDatabasePane {
"addTableEntityPane": AddTableEntityPane { "autoPilotUsageCost": [Function],
"addButtonLabel": "Add Property", "canConfigureThroughput": [Function],
"attributeNameLabel": "Property Name", "canExceedMaximumValue": [Function],
"attributeValueLabel": "Value", "canRequestSupport": [Function],
"canAdd": [Function],
"canApply": [Function],
"container": [Circular], "container": [Circular],
"dataTypeLabel": "Type", "costsVisible": [Function],
"displayedAttributes": [Function], "databaseCreateNewShared": [Function],
"editAttribute": [Function], "databaseId": [Function],
"editButtonLabel": "Edit", "databaseIdLabel": [Function],
"editingProperty": [Function], "databaseIdPlaceHolder": [Function],
"edmTypes": [Function], "databaseIdTooltipText": [Function],
"enterRequiredValueLabel": "Enter identifier value.", "databaseLevelThroughputTooltipText": [Function],
"enterValueLabel": "Enter value to keep property.",
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"id": "addtableentitypane", "freeTierExceedThroughputTooltip": [Function],
"insertAttribute": [Function], "id": "adddatabasepane",
"isEditing": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function], "isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function], "maxAutoPilotThroughputSet": [Function],
"onBackButtonKeyDown": [Function], "maxThroughputRU": [Function],
"onDeletePropertyKeyDown": [Function], "maxThroughputRUText": [Function],
"onEditPropertyKeyDown": [Function], "minThroughputRU": [Function],
"onKeyUp": [Function], "onMoreDetailsKeyPress": [Function],
"removeAttribute": [Function], "requestUnitsUsageCost": [Function],
"removeButtonLabel": "Remove", "ruToolTipText": [Function],
"scrollId": [Function], "showUpsellMessage": [Function],
"submitButtonText": [Function], "throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function], "title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function], "visible": [Function],
}, },
"addDatabaseText": [Function],
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -532,21 +453,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"databaseAccount": [Function], "databaseAccount": [Function],
"databases": [Function], "databases": [Function],
"defaultExperience": [Function], "defaultExperience": [Function],
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
"collectionIdConfirmation": [Function],
"collectionIdConfirmationText": [Function],
"container": [Circular],
"containerDeleteFeedback": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "deletecollectionconfirmationpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"recordDeleteFeedback": [Function],
"title": [Function],
"visible": [Function],
},
"deleteCollectionText": [Function], "deleteCollectionText": [Function],
"deleteDatabaseText": [Function], "deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane { "editTableEntityPane": EditTableEntityPane {
@@ -583,7 +489,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -605,10 +510,8 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isGitHubPaneEnabled": [Function], "isGitHubPaneEnabled": [Function],
@@ -617,11 +520,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isPreferredApiCassandra": [Function],
"isPreferredApiDocumentDB": [Function],
"isPreferredApiGraph": [Function],
"isPreferredApiMongoDB": [Function], "isPreferredApiMongoDB": [Function],
"isPreferredApiTable": [Function],
"isPublishNotebookPaneEnabled": [Function], "isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function], "isRightPanelV2Enabled": [Function],
@@ -632,22 +531,6 @@ exports[`AddDatabasePane Pane should render Default properly 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],
@@ -659,27 +542,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -716,20 +578,6 @@ exports[`AddDatabasePane Pane should render Default properly 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 {
@@ -758,32 +606,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
@@ -887,10 +709,12 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
</div> </div>
<div> <div>
<ThroughputInput <ThroughputInput
isAutoscaleSelected={false}
isDatabase={true} isDatabase={true}
onCostAcknowledgeChange={[Function]} onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]} setIsAutoscale={[Function]}
setThroughputValue={[Function]} setThroughputValue={[Function]}
throughput={400}
/> />
</div> </div>
</div> </div>

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

@@ -1,194 +0,0 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
import { IDropdownOption } from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export class CopyNotebookPaneAdapter implements ReactAdapter {
private static readonly BranchNameWhiteSpace = " ";
parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private name: string;
private content: string;
private pinnedRepos: IPinnedRepo[];
private selectedLocation: Location;
constructor(
private container: Explorer,
private junoClient: JunoClient,
private gitHubOAuthService: GitHubOAuthService
) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const genericPaneProps: GenericRightPaneProps = {
container: this.container,
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "copynotebookpane",
isExecuting: this.isExecuting,
title: "Copy notebook",
submitButtonText: "OK",
onClose: () => this.close(),
onSubmit: () => this.submit(),
};
const copyNotebookPaneProps: CopyNotebookPaneProps = {
name: this.name,
pinnedRepos: this.pinnedRepos,
onDropDownChange: this.onDropDownChange,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent>
);
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public async open(name: string, content: string): Promise<void> {
this.name = name;
this.content = content;
this.isOpened = true;
this.triggerRender();
if (this.gitHubOAuthService.isLoggedIn()) {
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
}
if (response.data?.length > 0) {
this.pinnedRepos = response.data;
this.triggerRender();
}
}
}
public close(): void {
this.reset();
this.triggerRender();
}
public async submit(): Promise<void> {
let destination: string = this.selectedLocation?.type;
let clearMessage: () => void;
this.isExecuting = true;
this.triggerRender();
try {
if (!this.selectedLocation) {
throw new Error(`No location selected`);
}
if (this.selectedLocation.type === "GitHub") {
destination = `${destination} - ${GitHubUtils.toRepoFullName(
this.selectedLocation.owner,
this.selectedLocation.repo
)} - ${this.selectedLocation.branch}`;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${this.name} to ${destination}`);
const notebookContentItem = await this.copyNotebook(this.selectedLocation);
if (!notebookContentItem) {
throw new Error(`Failed to upload ${this.name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
} catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to copy ${this.name} to ${destination}`;
this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", this.formError);
return;
} finally {
clearMessage && clearMessage();
this.isExecuting = false;
this.triggerRender();
}
this.close();
}
private copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem;
switch (location.type) {
case "MyNotebooks":
parent = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
break;
case "GitHub":
parent = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri(
this.selectedLocation.owner,
this.selectedLocation.repo,
this.selectedLocation.branch,
""
),
type: NotebookContentItemType.Directory,
};
break;
default:
throw new Error(`Unsupported location type ${location.type}`);
}
return this.container.uploadFile(this.name, this.content, parent);
};
private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
this.selectedLocation = option?.data;
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = undefined;
this.formErrorDetail = undefined;
this.name = undefined;
this.content = undefined;
this.pinnedRepos = undefined;
this.selectedLocation = undefined;
};
}

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 { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent"; 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>("");

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = ` exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
<DeleteCollectionConfirmationPanel <DeleteCollectionConfirmationPane
closePanel={[Function]} closePanel={[Function]}
collectionName="container" collectionName="container"
explorer={ explorer={
@@ -3627,5 +3627,5 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</div> </div>
</div> </div>
</GenericRightPaneComponent> </GenericRightPaneComponent>
</DeleteCollectionConfirmationPanel> </DeleteCollectionConfirmationPane>
`; `;

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,

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 {}}
@@ -1062,7 +1062,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<button <button
aria-label="Close pane" aria-label="Close pane"
className="ms-Button ms-Button--icon closePaneBtn root-72" className="ms-Button ms-Button--icon closePaneBtn root-40"
data-is-focusable={true} data-is-focusable={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -1075,16 +1075,16 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-73" className="ms-Button-flexContainer flexContainer-41"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<Component <Component
className="ms-Button-icon icon-75" className="ms-Button-icon icon-43"
iconName="Cancel" iconName="Cancel"
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Icon root-37 css-80 ms-Button-icon icon-75" className="ms-Icon root-37 css-48 ms-Button-icon icon-43"
data-icon-name="Cancel" data-icon-name="Cancel"
role="presentation" role="presentation"
style={ style={
@@ -1429,7 +1429,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-81" className="ms-Label root-49"
> >
Partition key value Partition key value
</label> </label>
@@ -1439,7 +1439,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<div <div
className="ms-Stack css-82" className="ms-Stack css-50"
> >
<StyledWithResponsiveMode <StyledWithResponsiveMode
key=".0:$.0" key=".0:$.0"
@@ -2336,7 +2336,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label ms-Dropdown-label root-99" className="ms-Label ms-Dropdown-label root-67"
id="Dropdown3-label" id="Dropdown3-label"
> >
Key Key
@@ -2348,7 +2348,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="listbox" aria-haspopup="listbox"
aria-labelledby="Dropdown3-label Dropdown3-option" aria-labelledby="Dropdown3-label Dropdown3-option"
className="ms-Dropdown dropdown-83" className="ms-Dropdown dropdown-51"
data-is-focusable={true} data-is-focusable={true}
id="Dropdown3" id="Dropdown3"
onBlur={[Function]} onBlur={[Function]}
@@ -2368,23 +2368,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-posinset={1} aria-posinset={1}
aria-selected={true} aria-selected={true}
aria-setsize={2} aria-setsize={2}
className="ms-Dropdown-title title-84" className="ms-Dropdown-title title-52"
id="Dropdown3-option" id="Dropdown3-option"
role="option" role="option"
> >
String String
</span> </span>
<span <span
className="ms-Dropdown-caretDownWrapper caretDownWrapper-85" className="ms-Dropdown-caretDownWrapper caretDownWrapper-53"
> >
<StyledIconBase <StyledIconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-86" className="ms-Dropdown-caretDown caretDown-54"
iconName="ChevronDown" iconName="ChevronDown"
> >
<IconBase <IconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-86" className="ms-Dropdown-caretDown caretDown-54"
iconName="ChevronDown" iconName="ChevronDown"
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -2663,7 +2663,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-100" className="ms-Dropdown-caretDown caretDown-68"
data-icon-name="ChevronDown" data-icon-name="ChevronDown"
> >
@@ -2969,7 +2969,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField root-102" className="ms-TextField root-70"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@@ -3258,7 +3258,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-81" className="ms-Label root-49"
htmlFor="confirmCollectionId" htmlFor="confirmCollectionId"
id="TextFieldLabel6" id="TextFieldLabel6"
> >
@@ -3267,13 +3267,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</LabelBase> </LabelBase>
</StyledLabelBase> </StyledLabelBase>
<div <div
className="ms-TextField-fieldGroup fieldGroup-103" className="ms-TextField-fieldGroup fieldGroup-71"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-labelledby="TextFieldLabel6" aria-labelledby="TextFieldLabel6"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-104" className="ms-TextField-field field-72"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -3581,7 +3581,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-81" className="ms-Label root-49"
> >
Enter input parameters (if any) Enter input parameters (if any)
</label> </label>
@@ -3591,7 +3591,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<div <div
className="ms-Stack css-82" className="ms-Stack css-50"
> >
<StyledWithResponsiveMode <StyledWithResponsiveMode
key=".0:$.0" key=".0:$.0"
@@ -4488,7 +4488,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label ms-Dropdown-label root-99" className="ms-Label ms-Dropdown-label root-67"
id="Dropdown7-label" id="Dropdown7-label"
> >
Key Key
@@ -4500,7 +4500,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="listbox" aria-haspopup="listbox"
aria-labelledby="Dropdown7-label Dropdown7-option" aria-labelledby="Dropdown7-label Dropdown7-option"
className="ms-Dropdown dropdown-83" className="ms-Dropdown dropdown-51"
data-is-focusable={true} data-is-focusable={true}
id="Dropdown7" id="Dropdown7"
onBlur={[Function]} onBlur={[Function]}
@@ -4520,23 +4520,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-posinset={1} aria-posinset={1}
aria-selected={true} aria-selected={true}
aria-setsize={2} aria-setsize={2}
className="ms-Dropdown-title title-84" className="ms-Dropdown-title title-52"
id="Dropdown7-option" id="Dropdown7-option"
role="option" role="option"
> >
String String
</span> </span>
<span <span
className="ms-Dropdown-caretDownWrapper caretDownWrapper-85" className="ms-Dropdown-caretDownWrapper caretDownWrapper-53"
> >
<StyledIconBase <StyledIconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-86" className="ms-Dropdown-caretDown caretDown-54"
iconName="ChevronDown" iconName="ChevronDown"
> >
<IconBase <IconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-86" className="ms-Dropdown-caretDown caretDown-54"
iconName="ChevronDown" iconName="ChevronDown"
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -4815,7 +4815,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-100" className="ms-Dropdown-caretDown caretDown-68"
data-icon-name="ChevronDown" data-icon-name="ChevronDown"
> >
@@ -5123,7 +5123,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
value="" value=""
> >
<div <div
className="ms-TextField root-102" className="ms-TextField root-70"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@@ -5412,7 +5412,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-81" className="ms-Label root-49"
htmlFor="confirmCollectionId" htmlFor="confirmCollectionId"
id="TextFieldLabel10" id="TextFieldLabel10"
> >
@@ -5421,13 +5421,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</LabelBase> </LabelBase>
</StyledLabelBase> </StyledLabelBase>
<div <div
className="ms-TextField-fieldGroup fieldGroup-103" className="ms-TextField-fieldGroup fieldGroup-71"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-labelledby="TextFieldLabel10" aria-labelledby="TextFieldLabel10"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-104" className="ms-TextField-field field-72"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -5735,7 +5735,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
width={20} width={20}
> >
<div <div
className="ms-Image addRemoveIconLabel root-113" className="ms-Image addRemoveIconLabel root-81"
style={ style={
Object { Object {
"height": 30, "height": 30,
@@ -5745,7 +5745,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<img <img
alt="Delete param" alt="Delete param"
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-114" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-82"
id="deleteparam" id="deleteparam"
key="fabricImage" key="fabricImage"
onClick={[Function]} onClick={[Function]}
@@ -6050,7 +6050,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
width={20} width={20}
> >
<div <div
className="ms-Image addRemoveIconLabel root-113" className="ms-Image addRemoveIconLabel root-81"
style={ style={
Object { Object {
"height": 30, "height": 30,
@@ -6060,7 +6060,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<img <img
alt="Add param" alt="Add param"
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-114" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-82"
id="addparam" id="addparam"
key="fabricImage" key="fabricImage"
onClick={[Function]} onClick={[Function]}
@@ -6079,7 +6079,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
onClick={[Function]} onClick={[Function]}
> >
<div <div
className="ms-Stack css-82" className="ms-Stack css-50"
onClick={[Function]} onClick={[Function]}
> >
<StyledImageBase <StyledImageBase
@@ -6371,7 +6371,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
width={20} width={20}
> >
<div <div
className="ms-Image root-113" className="ms-Image root-81"
style={ style={
Object { Object {
"height": 30, "height": 30,
@@ -6381,7 +6381,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<img <img
alt="Add param" alt="Add param"
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-114" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-82"
key="fabricImage" key="fabricImage"
onError={[Function]} onError={[Function]}
onLoad={[Function]} onLoad={[Function]}
@@ -6395,7 +6395,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
key=".0:$.1" key=".0:$.1"
> >
<span <span
className="addNewParamStyle css-115" className="addNewParamStyle css-83"
> >
Add New Param Add New Param
</span> </span>
@@ -8121,7 +8121,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<button <button
aria-label="Submit" aria-label="Submit"
className="ms-Button ms-Button--primary genericPaneSubmitBtn root-116" className="ms-Button ms-Button--primary genericPaneSubmitBtn root-84"
data-is-focusable={true} data-is-focusable={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -8139,14 +8139,14 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-73" className="ms-Button-flexContainer flexContainer-41"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<span <span
className="ms-Button-textContainer textContainer-74" className="ms-Button-textContainer textContainer-42"
> >
<span <span
className="ms-Button-label label-117" className="ms-Button-label label-85"
id="id__11" id="id__11"
key="id__11" key="id__11"
> >
@@ -8176,5 +8176,5 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</GenericRightPaneComponent> </GenericRightPaneComponent>
</ExecuteSprocParamsPanel> </ExecuteSprocParamsPane>
`; `;

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}`;
};
}

View File

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

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