mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-21 01:41:31 +00:00
resolve master branch conflict
This commit is contained in:
@@ -89,8 +89,7 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
|
||||
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
|
||||
src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.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.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.ts
|
||||
src/Explorer/Panes/GraphStylingPane.ts
|
||||
src/Explorer/Panes/NewVertexPane.ts
|
||||
# src/Explorer/Panes/NewVertexPane.ts
|
||||
src/Explorer/Panes/PaneComponents.ts
|
||||
src/Explorer/Panes/RenewAdHocAccessPane.ts
|
||||
src/Explorer/Panes/SetupNotebooksPane.ts
|
||||
src/Explorer/Panes/StringInputPane.ts
|
||||
src/Explorer/Panes/SwitchDirectoryPane.ts
|
||||
src/Explorer/Panes/Tables/AddTableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/EditTableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
|
||||
src/Explorer/Panes/Tables/TableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
|
||||
src/Explorer/Panes/AddDatabasePane.ts
|
||||
src/Explorer/SplashScreen/SplashScreen.test.ts
|
||||
src/Explorer/Tables/Constants.ts
|
||||
src/Explorer/Tables/DataTable/CacheBase.ts
|
||||
|
||||
71
.github/workflows/ci.yml
vendored
71
.github/workflows/ci.yml
vendored
@@ -101,7 +101,8 @@ jobs:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
endtoendemulator:
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -126,58 +127,21 @@ jobs:
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
accessibility:
|
||||
name: "Accessibility | Hosted"
|
||||
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]
|
||||
endtoend:
|
||||
name: "E2E"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
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 }}
|
||||
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:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test-file:
|
||||
- ./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/mongo/container.spec.ts
|
||||
- ./test/selfServe/selfServeExample.spec.ts
|
||||
- ./test/notebooks/upload.spec.ts
|
||||
- ./test/sql/resourceToken.spec.ts
|
||||
- ./test/tables/container.spec.ts
|
||||
steps:
|
||||
@@ -188,30 +152,17 @@ jobs:
|
||||
node-version: 14.x
|
||||
- run: npm ci
|
||||
- run: npm start &
|
||||
- run: node utils/cleanupDBs.js
|
||||
- run: npm run wait-for-server
|
||||
- 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
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
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
|
||||
path: screenshots/
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
|
||||
28
.github/workflows/cleanup.yml
vendored
Normal file
28
.github/workflows/cleanup.yml
vendored
Normal 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
2
.gitignore
vendored
@@ -15,3 +15,5 @@ Contracts/*
|
||||
.cache/
|
||||
.env
|
||||
failure.png
|
||||
screenshots/*
|
||||
GettingStarted-ignore*.ipynb
|
||||
@@ -153,7 +153,7 @@ Cosmos Explorer has been under constant development for over 5 years. As a resul
|
||||
|
||||
✅ 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.
|
||||
- Use caution. Do not try to cover every case. End to End tests can be slow and brittle.
|
||||
|
||||
|
||||
10
externals/iframeResizer.contentWindow.min.js
vendored
Normal file
10
externals/iframeResizer.contentWindow.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
jest-playwright.config.js
Normal file
13
jest-playwright.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const isCI = require("is-ci");
|
||||
|
||||
module.exports = {
|
||||
exitOnPageError: false,
|
||||
launchOptions: {
|
||||
headless: isCI,
|
||||
slowMo: 10,
|
||||
timeout: 60000,
|
||||
},
|
||||
contextOptions: {
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
preset: "jest-puppeteer",
|
||||
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
|
||||
setupFiles: ["dotenv/config"],
|
||||
};
|
||||
7
jest.config.playwright.js
Normal file
7
jest.config.playwright.js
Normal 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
5674
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -67,6 +67,7 @@
|
||||
"i18next": "19.8.4",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "1.0.23",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"is-ci": "2.0.0",
|
||||
"jquery": "3.5.1",
|
||||
@@ -80,6 +81,7 @@
|
||||
"office-ui-fabric-react": "7.164.2",
|
||||
"p-retry": "4.2.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
"react": "16.13.1",
|
||||
"react-animate-height": "2.0.8",
|
||||
@@ -113,15 +115,12 @@
|
||||
"@types/d3": "5.9.2",
|
||||
"@types/enzyme": "3.10.7",
|
||||
"@types/enzyme-adapter-react-16": "1.0.6",
|
||||
"@types/expect-puppeteer": "4.4.5",
|
||||
"@types/hasher": "0.0.31",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/jest-environment-puppeteer": "4.4.1",
|
||||
"@types/memoize-one": "4.1.1",
|
||||
"@types/node": "12.11.1",
|
||||
"@types/post-robot": "10.0.1",
|
||||
"@types/promise.prototype.finally": "2.0.3",
|
||||
"@types/prop-types": "15.5.8",
|
||||
"@types/puppeteer": "5.4.3",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "17.0.3",
|
||||
"@types/react-dom": "17.0.3",
|
||||
@@ -133,7 +132,6 @@
|
||||
"@types/underscore": "1.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "4.0.1",
|
||||
"@typescript-eslint/parser": "4.0.1",
|
||||
"axe-puppeteer": "1.1.0",
|
||||
"babel-jest": "24.9.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"buffer": "5.1.0",
|
||||
@@ -148,16 +146,18 @@
|
||||
"eslint-plugin-no-null": "1.0.2",
|
||||
"eslint-plugin-prefer-arrow": "1.2.2",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"expect-playwright": "0.3.3",
|
||||
"expose-loader": "0.7.5",
|
||||
"fast-glob": "3.2.5",
|
||||
"file-loader": "2.0.0",
|
||||
"fs-extra": "7.0.0",
|
||||
"html-inline-css-webpack-plugin": "1.11.0",
|
||||
"html-loader": "0.5.5",
|
||||
"html-loader-jest": "0.2.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"html-webpack-plugin": "4.5.2",
|
||||
"jest": "25.5.4",
|
||||
"jest-canvas-mock": "2.1.0",
|
||||
"jest-puppeteer": "4.4.0",
|
||||
"jest-playwright-preset": "1.5.1",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.1.0",
|
||||
@@ -165,16 +165,17 @@
|
||||
"mini-css-extract-plugin": "0.4.3",
|
||||
"monaco-editor-webpack-plugin": "1.7.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"playwright": "1.10.0",
|
||||
"prettier": "2.2.1",
|
||||
"puppeteer": "8.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"react-dev-utils": "11.0.4",
|
||||
"rimraf": "3.0.0",
|
||||
"sinon": "3.2.1",
|
||||
"style-loader": "0.23.0",
|
||||
"ts-loader": "6.2.2",
|
||||
"tslint": "5.11.0",
|
||||
"tslint-microsoft-contrib": "6.0.0",
|
||||
"typescript": "4.2.3",
|
||||
"typescript": "4.2.4",
|
||||
"url-loader": "1.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "4.43.0",
|
||||
|
||||
68
src/CellOutputViewer/CellOutputViewer.tsx
Normal file
68
src/CellOutputViewer/CellOutputViewer.tsx
Normal 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);
|
||||
138
src/CellOutputViewer/TransformMedia.tsx
Normal file
138
src/CellOutputViewer/TransformMedia.tsx
Normal 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;
|
||||
12
src/CellOutputViewer/cellOutputViewer.html
Normal file
12
src/CellOutputViewer/cellOutputViewer.html
Normal 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>
|
||||
1
src/CellOutputViewer/transforms/Vega2.ts
Normal file
1
src/CellOutputViewer/transforms/Vega2.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega2 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/Vega3.ts
Normal file
1
src/CellOutputViewer/transforms/Vega3.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega3 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/Vega4.ts
Normal file
1
src/CellOutputViewer/transforms/Vega4.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega4 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/Vega5.ts
Normal file
1
src/CellOutputViewer/transforms/Vega5.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega5 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite1.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite1.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite1 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite2.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite2.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite2 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite3.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite3.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite3 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite4.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite4.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite4 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/WidgetDisplay.ts
Normal file
1
src/CellOutputViewer/transforms/WidgetDisplay.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { WidgetDisplay as default } from "@nteract/jupyter-widgets";
|
||||
61
src/Common/EntityValue.tsx
Normal file
61
src/Common/EntityValue.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -5,11 +5,10 @@ import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@@ -348,10 +347,7 @@ export function getEndpoint(): string {
|
||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||
const errorMessage = await response.text();
|
||||
// Log the error where the user can see it
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`
|
||||
);
|
||||
logConsoleError(`Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}`);
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||
return;
|
||||
|
||||
136
src/Common/TableEntity.tsx
Normal file
136
src/Common/TableEntity.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Image, Stack, TextField } from "office-ui-fabric-react";
|
||||
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
|
||||
import FolderIcon from "../../../images/folder_16x16.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { Tooltip } from "../Tooltip";
|
||||
import * as Constants from "../Constants";
|
||||
import { Tooltip } from "../Tooltip/Tooltip";
|
||||
|
||||
interface UploadProps {
|
||||
label: string;
|
||||
@@ -26,6 +26,7 @@ export interface ConfigContext {
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
allowedJunoOrigins: string[];
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
@@ -53,6 +54,13 @@ let configContext: Readonly<ConfigContext> = {
|
||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.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 {
|
||||
@@ -86,13 +94,18 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
});
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
|
||||
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
|
||||
Object.assign(configContext, externalConfig);
|
||||
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
|
||||
});
|
||||
}
|
||||
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to parse json in config file");
|
||||
console.error(error);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
export enum TabKind {
|
||||
SQLDocuments,
|
||||
MongoDocuments,
|
||||
SchemaAnalyzer,
|
||||
TableEntities,
|
||||
Graph,
|
||||
SQLQuery,
|
||||
|
||||
@@ -141,6 +141,7 @@ export interface Collection extends CollectionBase {
|
||||
onTableEntitiesClick(): void;
|
||||
onGraphDocumentsClick(): void;
|
||||
onMongoDBDocumentsClick(): void;
|
||||
onSchemaAnalyzerClick(): void;
|
||||
openTab(): void;
|
||||
|
||||
onSettingsClick: () => Promise<void>;
|
||||
@@ -366,6 +367,7 @@ export enum CollectionTabKind {
|
||||
Schema = 19,
|
||||
CollectionSettingsV2 = 20,
|
||||
DatabaseSettingsV2 = 21,
|
||||
SchemaAnalyzer = 22,
|
||||
}
|
||||
|
||||
export enum TerminalKind {
|
||||
|
||||
@@ -8,10 +8,6 @@ describe("Component Registerer", () => {
|
||||
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", () => {
|
||||
expect(ko.components.isRegistered("error-display")).toBe(true);
|
||||
});
|
||||
@@ -24,59 +20,10 @@ describe("Component Registerer", () => {
|
||||
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", () => {
|
||||
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", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it("should register setup-notebooks-pane component", () => {
|
||||
expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register dynamic-list component", () => {
|
||||
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -7,26 +7,9 @@ import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahea
|
||||
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
|
||||
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
|
||||
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
|
||||
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
|
||||
import * 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("new-vertex-form", NewVertexComponent);
|
||||
ko.components.register("error-display", new ErrorDisplayComponent());
|
||||
ko.components.register("graph-style", GraphStyleComponent);
|
||||
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("dynamic-list", DynamicListComponent);
|
||||
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
|
||||
|
||||
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
||||
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("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
|
||||
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
|
||||
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
|
||||
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
|
||||
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
|
||||
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());
|
||||
|
||||
@@ -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 {
|
||||
ChoiceGroup,
|
||||
FontIcon,
|
||||
@@ -10,6 +5,11 @@ import {
|
||||
IProgressIndicatorProps,
|
||||
ProgressIndicator,
|
||||
} 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 {
|
||||
label: string;
|
||||
@@ -50,43 +50,52 @@ const DIALOG_TITLE_FONT_SIZE = "17px";
|
||||
const DIALOG_TITLE_FONT_WEIGHT = 400;
|
||||
const DIALOG_SUBTEXT_FONT_SIZE = "15px";
|
||||
|
||||
export class Dialog extends React.Component<DialogProps> {
|
||||
constructor(props: DialogProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
export const Dialog: FunctionComponent<DialogProps> = ({
|
||||
title,
|
||||
subText,
|
||||
isModal,
|
||||
visible,
|
||||
choiceGroupProps,
|
||||
textFieldProps,
|
||||
linkProps,
|
||||
progressIndicatorProps,
|
||||
primaryButtonText,
|
||||
secondaryButtonText,
|
||||
onPrimaryButtonClick,
|
||||
onSecondaryButtonClick,
|
||||
primaryButtonDisabled,
|
||||
type,
|
||||
showCloseButton,
|
||||
onDismiss,
|
||||
}: DialogProps) => {
|
||||
const dialogProps: IDialogProps = {
|
||||
hidden: !this.props.visible,
|
||||
hidden: !visible,
|
||||
dialogContentProps: {
|
||||
type: this.props.type || DialogType.normal,
|
||||
title: this.props.title,
|
||||
subText: this.props.subText,
|
||||
type: type || DialogType.normal,
|
||||
title,
|
||||
subText,
|
||||
styles: {
|
||||
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
|
||||
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE },
|
||||
},
|
||||
showCloseButton: this.props.showCloseButton || false,
|
||||
onDismiss: this.props.onDismiss,
|
||||
showCloseButton: showCloseButton || false,
|
||||
onDismiss,
|
||||
},
|
||||
modalProps: { isBlocking: this.props.isModal, isDarkOverlay: false },
|
||||
modalProps: { isBlocking: isModal, isDarkOverlay: false },
|
||||
minWidth: DIALOG_MIN_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 = {
|
||||
text: this.props.primaryButtonText,
|
||||
disabled: this.props.primaryButtonDisabled || false,
|
||||
onClick: this.props.onPrimaryButtonClick,
|
||||
text: primaryButtonText,
|
||||
disabled: primaryButtonDisabled || false,
|
||||
onClick: onPrimaryButtonClick,
|
||||
};
|
||||
const secondaryButtonProps: IButtonProps =
|
||||
this.props.secondaryButtonText && this.props.onSecondaryButtonClick
|
||||
secondaryButtonText && onSecondaryButtonClick
|
||||
? {
|
||||
text: this.props.secondaryButtonText,
|
||||
onClick: this.props.onSecondaryButtonClick,
|
||||
text: secondaryButtonText,
|
||||
onClick: onSecondaryButtonClick,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -106,5 +115,4 @@ export class Dialog extends React.Component<DialogProps> {
|
||||
</DialogFooter>
|
||||
</FluentDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -350,11 +350,11 @@ exports[`test render renders with filters 1`] = `
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="ms-ScrollablePane root-72"
|
||||
className="ms-ScrollablePane root-40"
|
||||
data-is-scrollable="true"
|
||||
>
|
||||
<div
|
||||
className="stickyAbove-74"
|
||||
className="stickyAbove-42"
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
@@ -365,7 +365,7 @@ exports[`test render renders with filters 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="ms-ScrollablePane--contentContainer contentContainer-73"
|
||||
className="ms-ScrollablePane--contentContainer contentContainer-41"
|
||||
data-is-scrollable={true}
|
||||
>
|
||||
<Sticky
|
||||
@@ -691,18 +691,18 @@ exports[`test render renders with filters 1`] = `
|
||||
validateOnLoad={true}
|
||||
>
|
||||
<div
|
||||
className="ms-TextField directoryListFilterTextBox root-78"
|
||||
className="ms-TextField directoryListFilterTextBox root-46"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-79"
|
||||
className="ms-TextField-fieldGroup fieldGroup-47"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Directory filter text box"
|
||||
className="ms-TextField-field field-80"
|
||||
className="ms-TextField-field field-48"
|
||||
id="TextField0"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -1900,7 +1900,7 @@ exports[`test render renders with filters 1`] = `
|
||||
>
|
||||
<button
|
||||
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}
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
@@ -1912,7 +1912,7 @@ exports[`test render renders with filters 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-90"
|
||||
className="ms-Button-flexContainer flexContainer-58"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<div
|
||||
@@ -1943,7 +1943,7 @@ exports[`test render renders with filters 1`] = `
|
||||
</List>
|
||||
</div>
|
||||
<div
|
||||
className="stickyBelow-75"
|
||||
className="stickyBelow-43"
|
||||
style={
|
||||
Object {
|
||||
"bottom": "0px",
|
||||
@@ -1954,7 +1954,7 @@ exports[`test render renders with filters 1`] = `
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="stickyBelowItems-76"
|
||||
className="stickyBelowItems-44"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -321,7 +321,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
public getPartitionKeyVisible = (): boolean => {
|
||||
if (
|
||||
userContext.apiType === "Cassandra" ||
|
||||
this.props.container.isPreferredApiTable() ||
|
||||
userContext.apiType === "Tables" ||
|
||||
!this.props.collection.partitionKeyProperty ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
|
||||
) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import React from "react";
|
||||
import { ThroughputInput } from ".";
|
||||
import { ThroughputInput } from "./ThroughputInput";
|
||||
const props = {
|
||||
isDatabase: false,
|
||||
showFreeTierExceedThroughputTooltip: true,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "office-ui-fabric-react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Tooltip } from "../../../Common/Tooltip";
|
||||
import { Tooltip } from "../../../Common/Tooltip/Tooltip";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
@@ -15,7 +15,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
>
|
||||
<div
|
||||
className="ms-Stack css-72"
|
||||
className="ms-Stack css-40"
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
@@ -33,7 +33,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-73"
|
||||
className="css-41"
|
||||
style={
|
||||
Object {
|
||||
"lineHeight": "20px",
|
||||
@@ -1377,7 +1377,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Info"
|
||||
className="ms-Button ms-Button--icon root-74"
|
||||
className="ms-Button ms-Button--icon root-42"
|
||||
data-is-focusable={true}
|
||||
id="iconButton1"
|
||||
onClick={[Function]}
|
||||
@@ -1389,16 +1389,16 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-75"
|
||||
className="ms-Button-flexContainer flexContainer-43"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<Component
|
||||
className="ms-Button-icon icon-77"
|
||||
className="ms-Button-icon icon-45"
|
||||
iconName="Info"
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="presentation"
|
||||
style={
|
||||
@@ -1425,7 +1425,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</Stack>
|
||||
<Stack>
|
||||
<div
|
||||
className="ms-Stack css-83"
|
||||
className="ms-Stack css-51"
|
||||
>
|
||||
<StyledChoiceGroupBase
|
||||
aria-label="mode"
|
||||
@@ -1741,7 +1741,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className=""
|
||||
>
|
||||
<div
|
||||
className="ms-ChoiceFieldGroup root-84"
|
||||
className="ms-ChoiceFieldGroup root-52"
|
||||
role="radiogroup"
|
||||
>
|
||||
<div
|
||||
@@ -2051,14 +2051,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="ms-ChoiceField root-85"
|
||||
className="ms-ChoiceField root-53"
|
||||
>
|
||||
<div
|
||||
className="ms-ChoiceField-wrapper"
|
||||
>
|
||||
<input
|
||||
checked={true}
|
||||
className="ms-ChoiceField-input input-86"
|
||||
className="ms-ChoiceField-input input-54"
|
||||
id="ChoiceGroup6-true"
|
||||
name="ChoiceGroup6"
|
||||
onBlur={[Function]}
|
||||
@@ -2067,7 +2067,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="ms-ChoiceField-field is-checked field-87"
|
||||
className="ms-ChoiceField-field is-checked field-55"
|
||||
htmlFor="ChoiceGroup6-true"
|
||||
>
|
||||
<span
|
||||
@@ -2385,14 +2385,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="ms-ChoiceField root-85"
|
||||
className="ms-ChoiceField root-53"
|
||||
>
|
||||
<div
|
||||
className="ms-ChoiceField-wrapper"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="ms-ChoiceField-input input-86"
|
||||
className="ms-ChoiceField-input input-54"
|
||||
id="ChoiceGroup6-false"
|
||||
name="ChoiceGroup6"
|
||||
onBlur={[Function]}
|
||||
@@ -2401,7 +2401,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="ms-ChoiceField-field field-92"
|
||||
className="ms-ChoiceField-field field-60"
|
||||
htmlFor="ChoiceGroup6-false"
|
||||
>
|
||||
<span
|
||||
@@ -2426,7 +2426,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="throughputInputSpacing"
|
||||
>
|
||||
<div
|
||||
className="ms-Stack throughputInputSpacing css-83"
|
||||
className="ms-Stack throughputInputSpacing css-51"
|
||||
>
|
||||
<Text
|
||||
data-testid="ruDescription"
|
||||
@@ -2434,7 +2434,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-73"
|
||||
className="css-41"
|
||||
data-testid="ruDescription"
|
||||
>
|
||||
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
|
||||
className="ms-Link root-95"
|
||||
className="ms-Link root-63"
|
||||
data-testid="ruDescription"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
onClick={[Function]}
|
||||
@@ -2741,7 +2741,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
key=".0:$.1"
|
||||
>
|
||||
<div
|
||||
className="ms-Stack css-72"
|
||||
className="ms-Stack css-40"
|
||||
>
|
||||
<Text
|
||||
data-testid="maxRUDescription"
|
||||
@@ -2754,7 +2754,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-73"
|
||||
className="css-41"
|
||||
data-testid="maxRUDescription"
|
||||
style={
|
||||
Object {
|
||||
@@ -4101,7 +4101,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Info"
|
||||
className="ms-Button ms-Button--icon root-74"
|
||||
className="ms-Button ms-Button--icon root-42"
|
||||
data-is-focusable={true}
|
||||
id="iconButton9"
|
||||
onClick={[Function]}
|
||||
@@ -4113,16 +4113,16 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-75"
|
||||
className="ms-Button-flexContainer flexContainer-43"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<Component
|
||||
className="ms-Button-icon icon-77"
|
||||
className="ms-Button-icon icon-45"
|
||||
iconName="Info"
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="presentation"
|
||||
style={
|
||||
@@ -4456,17 +4456,17 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
value="4000"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField is-required root-97"
|
||||
className="ms-TextField is-required root-65"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-98"
|
||||
className="ms-TextField-fieldGroup fieldGroup-66"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-99"
|
||||
className="ms-TextField-field field-67"
|
||||
id="TextField14"
|
||||
min={4000}
|
||||
onBlur={[Function]}
|
||||
@@ -4488,7 +4488,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
className="css-73"
|
||||
className="css-41"
|
||||
>
|
||||
Your
|
||||
container
|
||||
@@ -15,7 +15,6 @@ describe("ContainerSampleGenerator", () => {
|
||||
const explorerStub = {} as Explorer;
|
||||
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
|
||||
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
|
||||
explorerStub.findDatabaseWithId = () => database;
|
||||
explorerStub.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
|
||||
export class DataSamplesUtil {
|
||||
@@ -21,18 +20,16 @@ export class DataSamplesUtil {
|
||||
if (this.hasContainer(databaseName, containerName, this.container.databases())) {
|
||||
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
|
||||
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logConsoleError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
await generator
|
||||
.createSampleContainerAsync()
|
||||
.catch((error) =>
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Error creating sample container: ${error}`)
|
||||
);
|
||||
.catch((error) => logConsoleError(`Error creating sample container: ${error}`));
|
||||
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
|
||||
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logConsoleInfo(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,43 +36,47 @@ import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationU
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PricingUtils from "../Utils/PricingUtils";
|
||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||
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 { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import type NotebookManager from "./Notebook/NotebookManager";
|
||||
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import AddCollectionPane from "./Panes/AddCollectionPane";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import AddDatabasePane from "./Panes/AddDatabasePane";
|
||||
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel";
|
||||
import { AddDatabasePanel } from "./Panes/AddDatabasePanelF/AddDatabasePanelF";
|
||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
||||
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
|
||||
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||
import { LoadQueryPanel } from "./Panes/LoadQueryPanel";
|
||||
import NewVertexPane from "./Panes/NewVertexPane";
|
||||
import { SaveQueryPanel } from "./Panes/SaveQueryPanel";
|
||||
import { SettingsPane } from "./Panes/SettingsPane";
|
||||
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
|
||||
import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane";
|
||||
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
|
||||
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
|
||||
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import type { GalleryTabOptions } from "./Tabs/GalleryTab";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
import TabsBase from "./Tabs/TabsBase";
|
||||
import QueryTablesTab from "./Tabs/QueryTablesTab";
|
||||
import { TabsManager } from "./Tabs/TabsManager";
|
||||
import TerminalTab from "./Tabs/TerminalTab";
|
||||
import Database from "./Tree/Database";
|
||||
@@ -122,12 +126,6 @@ export default class Explorer {
|
||||
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
|
||||
* */
|
||||
public isPreferredApiMongoDB: ko.Computed<boolean>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
|
||||
* */
|
||||
public isPreferredApiTable: ko.Computed<boolean>;
|
||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||
/**
|
||||
* @deprecated
|
||||
@@ -170,28 +168,21 @@ export default class Explorer {
|
||||
|
||||
// Tabs
|
||||
public isTabsContentExpanded: ko.Observable<boolean>;
|
||||
public galleryTab: any;
|
||||
public notebookViewerTab: any;
|
||||
public tabsManager: TabsManager;
|
||||
|
||||
// Contextual panes
|
||||
public addDatabasePane: AddDatabasePane;
|
||||
public addCollectionPane: AddCollectionPane;
|
||||
public graphStylingPane: GraphStylingPane;
|
||||
public addTableEntityPane: AddTableEntityPane;
|
||||
public editTableEntityPane: EditTableEntityPane;
|
||||
public newVertexPane: NewVertexPane;
|
||||
public cassandraAddCollectionPane: CassandraAddCollectionPane;
|
||||
public stringInputPane: StringInputPane;
|
||||
public setupNotebooksPane: SetupNotebooksPane;
|
||||
public gitHubReposPane: ContextualPaneBase;
|
||||
public publishNotebookPaneAdapter: ReactAdapter;
|
||||
public copyNotebookPaneAdapter: ReactAdapter;
|
||||
|
||||
// features
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
public isMongoIndexingEnabled: ko.Observable<boolean>;
|
||||
@@ -213,7 +204,7 @@ export default class Explorer {
|
||||
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
|
||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
public notebookManager?: any; // This is dynamically loaded
|
||||
public notebookManager?: NotebookManager;
|
||||
public openDialog: ExplorerParams["openDialog"];
|
||||
public closeDialog: ExplorerParams["closeDialog"];
|
||||
|
||||
@@ -289,7 +280,6 @@ export default class Explorer {
|
||||
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
|
||||
userContext.features.enableNotebooks)
|
||||
);
|
||||
|
||||
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
||||
isNotebookEnabled: this.isNotebookEnabled(),
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
@@ -342,7 +332,6 @@ export default class Explorer {
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isMongoIndexingEnabled = ko.observable<boolean>(false);
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
|
||||
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(() => {
|
||||
if (userContext.features.enableFixedCollectionWithSharedThroughput) {
|
||||
return true;
|
||||
@@ -503,7 +487,7 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.addCollectionPane = new AddCollectionPane({
|
||||
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
|
||||
isPreferredApiTable: ko.computed(() => userContext.apiType === "Tables"),
|
||||
id: "addcollectionpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
@@ -517,13 +501,6 @@ export default class Explorer {
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.addTableEntityPane = new AddTableEntityPane({
|
||||
id: "addtableentitypane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.editTableEntityPane = new EditTableEntityPane({
|
||||
id: "edittableentitypane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
@@ -531,13 +508,6 @@ export default class Explorer {
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.newVertexPane = new NewVertexPane({
|
||||
id: "newvertexpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
|
||||
id: "cassandraaddcollectionpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
@@ -552,13 +522,6 @@ export default class Explorer {
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.setupNotebooksPane = new SetupNotebooksPane({
|
||||
id: "setupnotebookspane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.tabsManager = params?.tabsManager ?? new TabsManager();
|
||||
this.tabsManager.openedTabs.subscribe((tabs) => {
|
||||
if (tabs.length === 0) {
|
||||
@@ -571,12 +534,9 @@ export default class Explorer {
|
||||
this.addDatabasePane,
|
||||
this.addCollectionPane,
|
||||
this.graphStylingPane,
|
||||
this.addTableEntityPane,
|
||||
this.editTableEntityPane,
|
||||
this.newVertexPane,
|
||||
this.cassandraAddCollectionPane,
|
||||
this.stringInputPane,
|
||||
this.setupNotebooksPane,
|
||||
];
|
||||
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
|
||||
this.isTabsContentExpanded = ko.observable(false);
|
||||
@@ -645,7 +605,6 @@ export default class Explorer {
|
||||
this.addCollectionPane.collectionIdTitle("Table id");
|
||||
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
|
||||
this.refreshTreeTitle("Refresh tables");
|
||||
this.addTableEntityPane.title("Add Table Entity");
|
||||
this.editTableEntityPane.title("Edit Table Entity");
|
||||
this.tableDataClient = new TablesAPIDataClient();
|
||||
break;
|
||||
@@ -660,7 +619,6 @@ export default class Explorer {
|
||||
this.addCollectionPane.collectionIdTitle("Table id");
|
||||
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
|
||||
this.refreshTreeTitle("Refresh tables");
|
||||
this.addTableEntityPane.title("Add Table Row");
|
||||
this.editTableEntityPane.title("Edit Table Row");
|
||||
this.tableDataClient = new CassandraAPIDataClient();
|
||||
break;
|
||||
@@ -679,10 +637,10 @@ export default class Explorer {
|
||||
this.isNotebookEnabled = ko.observable(false);
|
||||
this.isNotebookEnabled.subscribe(async () => {
|
||||
if (!this.notebookManager) {
|
||||
const notebookManagerModule = await import(
|
||||
/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"
|
||||
);
|
||||
this.notebookManager = new notebookManagerModule.default();
|
||||
const NotebookManager = await (
|
||||
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
|
||||
).default;
|
||||
this.notebookManager = new NotebookManager();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
notebookBasePath: this.notebookBasePath,
|
||||
@@ -756,8 +714,7 @@ export default class Explorer {
|
||||
|
||||
onPrimaryButtonClick: async () => {
|
||||
const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink);
|
||||
const logId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
const clearInProgressMessage = logConsoleProgress(
|
||||
"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);
|
||||
@@ -775,19 +732,13 @@ export default class Explorer {
|
||||
},
|
||||
}
|
||||
);
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(logId);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
"Enabled Azure Synapse Link for this account"
|
||||
);
|
||||
clearInProgressMessage();
|
||||
logConsoleInfo("Enabled Azure Synapse Link for this account");
|
||||
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
|
||||
this.databaseAccount(databaseAccount);
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(logId);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`
|
||||
);
|
||||
clearInProgressMessage();
|
||||
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
|
||||
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
|
||||
} finally {
|
||||
this.isSynapseLinkUpdating(false);
|
||||
@@ -914,10 +865,7 @@ export default class Explorer {
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${errorMessage}`
|
||||
);
|
||||
logConsoleError(`Error while refreshing databases: ${errorMessage}`);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1138,20 +1086,20 @@ export default class Explorer {
|
||||
|
||||
private _resetNotebookWorkspace = async () => {
|
||||
this._closeModalDialog();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace");
|
||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||
try {
|
||||
await this.notebookManager?.notebookClient.resetWorkspace();
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace");
|
||||
logConsoleInfo("Successfully reset notebook workspace");
|
||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`);
|
||||
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
||||
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
clearInProgressMessage();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1477,7 +1425,11 @@ export default class Explorer {
|
||||
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) {
|
||||
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
@@ -1486,11 +1438,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public copyNotebook(name: string, content: string): void {
|
||||
if (this.notebookManager) {
|
||||
this.notebookManager.openCopyNotebookPane(name, content);
|
||||
this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter;
|
||||
this.isCopyNotebookPaneEnabled(true);
|
||||
}
|
||||
this.notebookManager?.openCopyNotebookPane(name, content);
|
||||
}
|
||||
|
||||
public showOkModalDialog(title: string, msg: string): void {
|
||||
@@ -1708,11 +1656,7 @@ export default class Explorer {
|
||||
clearMessage();
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Could not download notebook ${getErrorMessage(error)}`
|
||||
);
|
||||
|
||||
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
|
||||
clearMessage();
|
||||
}
|
||||
);
|
||||
@@ -1864,15 +1808,8 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`);
|
||||
},
|
||||
(reason: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to delete "${item.path}": ${JSON.stringify(reason)}`
|
||||
);
|
||||
}
|
||||
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
|
||||
(reason: any) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1888,11 +1825,7 @@ export default class Explorer {
|
||||
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
|
||||
const notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating new notebook in ${parent.path}`
|
||||
);
|
||||
|
||||
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
@@ -1900,7 +1833,7 @@ export default class Explorer {
|
||||
this.notebookManager?.notebookContentClient
|
||||
.createNewNotebookFile(parent)
|
||||
.then((newFile: NotebookContentItem) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`);
|
||||
logConsoleInfo(`Successfully created: ${newFile.name}`);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.CreateNewNotebook,
|
||||
{
|
||||
@@ -1913,7 +1846,7 @@ export default class Explorer {
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch((error: any) => {
|
||||
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage);
|
||||
logConsoleError(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateNewNotebook,
|
||||
{
|
||||
@@ -1924,7 +1857,7 @@ export default class Explorer {
|
||||
startKey
|
||||
);
|
||||
})
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId));
|
||||
.finally(clearInProgressMessage);
|
||||
}
|
||||
|
||||
public refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||
@@ -1994,86 +1927,66 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async openGallery(
|
||||
selectedTab?: GalleryTab,
|
||||
selectedTab?: GalleryTabKind,
|
||||
notebookUrl?: string,
|
||||
galleryItem?: IGalleryItem,
|
||||
isFavorite?: boolean
|
||||
) {
|
||||
let title: string = "Gallery";
|
||||
let hashLocation: string = "gallery";
|
||||
const title = "Gallery";
|
||||
const hashLocation = "gallery";
|
||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||
|
||||
const galleryTabOptions: any = {
|
||||
// GalleryTabOptions
|
||||
const galleryTabOptions: GalleryTabOptions = {
|
||||
account: userContext.databaseAccount,
|
||||
container: this,
|
||||
junoClient: this.notebookManager?.junoClient,
|
||||
selectedTab: selectedTab || GalleryTab.PublicGallery,
|
||||
selectedTab: selectedTab || GalleryTabKind.PublicGallery,
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite,
|
||||
// TabOptions
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
isActive: ko.observable(false),
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
};
|
||||
|
||||
const galleryTabs = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Gallery,
|
||||
(tab) => tab.hashLocation() == hashLocation
|
||||
);
|
||||
let galleryTab = galleryTabs && galleryTabs[0];
|
||||
const galleryTab = this.tabsManager
|
||||
.getTabs(ViewModels.CollectionTabKind.Gallery)
|
||||
.find((tab) => tab.hashLocation() == hashLocation);
|
||||
|
||||
if (galleryTab) {
|
||||
if (galleryTab instanceof GalleryTab) {
|
||||
this.tabsManager.activateTab(galleryTab);
|
||||
(galleryTab as any).reset(galleryTabOptions);
|
||||
galleryTab.reset(galleryTabOptions);
|
||||
} else {
|
||||
if (!this.galleryTab) {
|
||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||
}
|
||||
const newTab = new this.galleryTab.default(galleryTabOptions);
|
||||
this.tabsManager.activateNewTab(newTab);
|
||||
this.tabsManager.activateNewTab(new GalleryTab(galleryTabOptions));
|
||||
}
|
||||
}
|
||||
|
||||
public async openNotebookViewer(notebookUrl: string) {
|
||||
const title = path.basename(notebookUrl);
|
||||
const hashLocation = notebookUrl;
|
||||
const NotebookViewerTab = await (
|
||||
await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab")
|
||||
).default;
|
||||
|
||||
if (!this.notebookViewerTab) {
|
||||
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab");
|
||||
}
|
||||
|
||||
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);
|
||||
const notebookViewerTab = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2).find((tab) => {
|
||||
return tab.hashLocation() == hashLocation && tab instanceof NotebookViewerTab && tab.notebookUrl === notebookUrl;
|
||||
});
|
||||
let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0];
|
||||
|
||||
if (notebookViewerTab) {
|
||||
this.tabsManager.activateNewTab(notebookViewerTab);
|
||||
} else {
|
||||
notebookViewerTab = new this.notebookViewerTab.default({
|
||||
const notebookViewerTab = new NotebookViewerTab({
|
||||
account: userContext.databaseAccount,
|
||||
tabKind: ViewModels.CollectionTabKind.NotebookViewer,
|
||||
node: null,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
collection: null,
|
||||
hashLocation: hashLocation,
|
||||
isActive: ko.observable(false),
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
@@ -2134,7 +2047,7 @@ export default class Explorer {
|
||||
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.";
|
||||
|
||||
this.setupNotebooksPane.openWithTitleAndDescription(title, description);
|
||||
this.openSetupNotebooksPanel(title, description);
|
||||
}
|
||||
|
||||
public async handleOpenFileAction(path: string): Promise<void> {
|
||||
@@ -2199,7 +2112,7 @@ export default class Explorer {
|
||||
let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience);
|
||||
this.openSidePanel(
|
||||
"Delete " + collectionName,
|
||||
<DeleteCollectionConfirmationPanel
|
||||
<DeleteCollectionConfirmationPane
|
||||
explorer={this}
|
||||
collectionName={collectionName}
|
||||
closePanel={this.closeSidePanel}
|
||||
@@ -2230,7 +2143,7 @@ export default class Explorer {
|
||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||
this.openSidePanel(
|
||||
"Input parameters",
|
||||
<ExecuteSprocParamsPanel
|
||||
<ExecuteSprocParamsPane
|
||||
explorer={this}
|
||||
storedProcedure={storedProcedure}
|
||||
closePanel={() => this.closeSidePanel()}
|
||||
@@ -2257,15 +2170,15 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
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 {
|
||||
this.openSidePanel("Load Query", <LoadQueryPanel explorer={this} closePanel={() => this.closeSidePanel()} />);
|
||||
this.openSidePanel("Load Query", <LoadQueryPane explorer={this} closePanel={() => this.closeSidePanel()} />);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -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 {
|
||||
this.openSidePanel(
|
||||
"Select Column",
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import * as ko from "knockout";
|
||||
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 { BaseType } from "d3";
|
||||
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 { 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 { 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 {
|
||||
[key: string]: { data: string; format: string };
|
||||
@@ -1005,7 +1003,7 @@ export class D3ForceGraph implements GraphRenderer {
|
||||
*/
|
||||
private loadNeighbors(v: D3Node, pageAction: PAGE_ACTION) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import * as Q from "q";
|
||||
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 { 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 * as Constants from "../../../Common/Constants";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
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 {
|
||||
applyFilter: () => void;
|
||||
@@ -697,13 +696,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @param cmd
|
||||
*/
|
||||
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);
|
||||
|
||||
return this.gremlinClient.execute(cmd).then(
|
||||
(result: GremlinClient.GremlinRequestResult) => {
|
||||
this.setExecuteCounter(this.executeCounter - 1);
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
clearConsoleProgress();
|
||||
if (result.isIncomplete) {
|
||||
const msg = `The query results are too large and only partial results are displayed for: ${cmd}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, msg);
|
||||
@@ -718,7 +717,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
(err: string) => {
|
||||
this.setExecuteCounter(this.executeCounter - 1);
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Gremlin query failed: ${cmd}`, err);
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
clearConsoleProgress();
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
@@ -1083,13 +1082,26 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @param errorData additional errors
|
||||
* @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 = "";
|
||||
if (errorData && errorData.length > 0) {
|
||||
console.error(msg, 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) {
|
||||
@@ -1368,7 +1380,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
let { id } = d;
|
||||
if (typeof id !== "string") {
|
||||
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
|
||||
logConsoleError(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
@@ -1380,7 +1392,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
pk = pk[0]["_value"];
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1767,7 +1779,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${
|
||||
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 {
|
||||
const results: ViewModels.QueryResults = await queryDocumentsPage(
|
||||
@@ -1776,7 +1791,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.currentDocDBQueryInfo.index
|
||||
);
|
||||
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
clearConsoleProgress();
|
||||
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
|
||||
this.setState({ hasMoreRoots: results.hasMoreResults });
|
||||
RU = results.requestCharge.toString();
|
||||
@@ -1793,7 +1808,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
|
||||
return { requestCharge: RU };
|
||||
} catch (error) {
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
clearConsoleProgress();
|
||||
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||
this.setState({
|
||||
@@ -2003,8 +2018,4 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private static clearConsoleProgress(id: string) {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
*/
|
||||
|
||||
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 { HashMap } from "../../../Common/HashMap";
|
||||
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
|
||||
|
||||
export interface GremlinClientParameters {
|
||||
endpoint: string;
|
||||
@@ -77,9 +76,7 @@ export class GremlinClient {
|
||||
this.abortPendingRequest(requestId, errorMessage, result.requestCharge);
|
||||
}
|
||||
},
|
||||
infoCallback: (msg: string) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
},
|
||||
infoCallback: logConsoleInfo,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
213
src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx
Normal file
213
src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`New Vertex Component should render default prpoerty 1`] = `[Function]`;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
const toWatch = [
|
||||
container.isPreferredApiTable,
|
||||
container.isPreferredApiMongoDB,
|
||||
container.deleteCollectionText,
|
||||
container.deleteDatabaseText,
|
||||
|
||||
@@ -16,7 +16,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
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.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
@@ -54,7 +60,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
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.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
@@ -117,7 +129,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
@@ -195,8 +213,15 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockExplorer.addDatabaseText = 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.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
@@ -226,6 +251,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
console.log(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
@@ -288,7 +314,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
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.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
@@ -47,7 +47,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
||||
buttons.push(addSynapseLink);
|
||||
}
|
||||
|
||||
if (!container.isPreferredApiTable()) {
|
||||
if (userContext.apiType !== "Tables") {
|
||||
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
||||
const newDatabaseBtn = createNewDatabase(container);
|
||||
newCollectionBtn.children.push(newDatabaseBtn);
|
||||
@@ -446,7 +446,7 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.setupNotebooksPane.openWithTitleAndDescription(label, description),
|
||||
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !container.isNotebooksEnabledForAccount(),
|
||||
@@ -483,7 +483,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
if (container.isNotebookEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -509,7 +509,7 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
if (container.isNotebookEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
|
||||
@@ -34,8 +34,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import configureStore from "./NotebookComponent/store";
|
||||
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
|
||||
import SandboxJavaScript from "./NotebookRenderer/outputs/SandboxJavaScript";
|
||||
import SanitizedHTML from "./NotebookRenderer/outputs/SanitizedHTML";
|
||||
|
||||
export type KernelSpecsDisplay = { name: string; displayName: string };
|
||||
|
||||
@@ -125,7 +123,9 @@ export class NotebookClientV2 {
|
||||
contents: makeContentsRecord({
|
||||
byRef: Immutable.Map<string, ContentRecord>(),
|
||||
}),
|
||||
transforms: makeTransformsRecord({
|
||||
transforms: userContext.features.sandboxNotebookOutputs
|
||||
? undefined
|
||||
: makeTransformsRecord({
|
||||
displayOrder: Immutable.List([
|
||||
"application/vnd.jupyter.widget-view+json",
|
||||
"application/vnd.vega.v5+json",
|
||||
@@ -168,10 +168,8 @@ export class NotebookClientV2 {
|
||||
"application/vnd.vega.v5+json": NullTransform,
|
||||
"application/vdom.v1+json": TransformVDOM,
|
||||
"application/json": Media.Json,
|
||||
"application/javascript": userContext.features.sandboxNotebookOutputs
|
||||
? SandboxJavaScript
|
||||
: Media.JavaScript,
|
||||
"text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : Media.HTML,
|
||||
"application/javascript": Media.JavaScript,
|
||||
"text/html": Media.HTML,
|
||||
"text/markdown": Media.Markdown,
|
||||
"text/latex": Media.LaTeX,
|
||||
"image/svg+xml": Media.SVG,
|
||||
@@ -202,10 +200,11 @@ export class NotebookClientV2 {
|
||||
case actions.FETCH_KERNELSPECS_FULFILLED: {
|
||||
const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload;
|
||||
const defaultKernelName = payload.defaultKernelName;
|
||||
this.kernelSpecsForDisplay = Object.keys(payload.kernelspecs)
|
||||
.map((name) => ({
|
||||
name,
|
||||
displayName: payload.kernelspecs[name].displayName,
|
||||
this.kernelSpecsForDisplay = Object.values(payload.kernelspecs)
|
||||
.filter((spec) => !spec.metadata?.hasOwnProperty("hidden"))
|
||||
.map((spec) => ({
|
||||
name: spec.name,
|
||||
displayName: spec.displayName,
|
||||
}))
|
||||
.sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => {
|
||||
// Put default at the top, otherwise lexicographically compare
|
||||
|
||||
@@ -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 {
|
||||
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,
|
||||
AppState,
|
||||
castToSessionId,
|
||||
ContentRef,
|
||||
createKernelRef,
|
||||
JupyterHostRecordProps,
|
||||
KernelInfo,
|
||||
KernelRef,
|
||||
RemoteKernelProps,
|
||||
selectors,
|
||||
ServerConfig as JupyterServerConfig,
|
||||
} from "@nteract/core";
|
||||
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
|
||||
import { sessions, kernels } from "rx-jupyter";
|
||||
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
||||
import { RecordOf } from "immutable";
|
||||
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 NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as CdbActions from "./actions";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
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 * as TextFile from "./contents/file/text-file";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as FileSystemUtil from "../FileSystemUtil";
|
||||
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 {
|
||||
userPuid?: string;
|
||||
@@ -311,7 +308,7 @@ export const launchWebSocketKernelEpic = (
|
||||
if (currentKernelspecs) {
|
||||
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
|
||||
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);
|
||||
} else {
|
||||
return of(
|
||||
@@ -337,7 +334,7 @@ export const launchWebSocketKernelEpic = (
|
||||
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
|
||||
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logConsoleInfo(msg);
|
||||
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
}
|
||||
|
||||
@@ -634,7 +631,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
case actions.RESTART_KERNEL_SUCCESSFUL: {
|
||||
const title = "Kernel restart";
|
||||
const msg = "Kernel successfully restarted";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logConsoleInfo(msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
@@ -645,7 +642,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
case actions.SAVE_FAILED: {
|
||||
const title = "Save failure";
|
||||
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logConsoleError(msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
@@ -654,7 +651,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
const filepath = selectors.filepath(state$.value, { contentRef: typedAction.payload.contentRef });
|
||||
const title = "Fetching content failure";
|
||||
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logConsoleError(msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
@@ -679,7 +676,7 @@ const handleKernelConnectionLostEpic = (
|
||||
const state = state$.value;
|
||||
|
||||
const msg = "Notebook was disconnected from kernel";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logConsoleError(msg);
|
||||
logFailureToTelemetry(state, "Error", "Kernel connection error");
|
||||
|
||||
const host = selectors.currentHost(state);
|
||||
@@ -692,7 +689,7 @@ const handleKernelConnectionLostEpic = (
|
||||
if (delayMs > Constants.Notebook.kernelRestartMaxDelayMs) {
|
||||
const msg =
|
||||
"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);
|
||||
|
||||
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.`;
|
||||
explorer.showOkModalDialog("File cannot be rendered", msg);
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logConsoleError(msg);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
@@ -838,7 +835,7 @@ const closeContentFailedToFetchEpic = (
|
||||
);
|
||||
const msg = `Failed to load file: ${filepath}.`;
|
||||
explorer.showOkModalDialog("Failure to load", msg);
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logConsoleError(msg);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/**
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
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 * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export class NotebookContainerClient {
|
||||
private reconnectingNotificationId: string;
|
||||
private clearReconnectionAttemptMessage? = () => {};
|
||||
private isResettingWorkspace: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -61,9 +60,9 @@ export class NotebookContainerClient {
|
||||
},
|
||||
});
|
||||
if (response.ok) {
|
||||
if (this.reconnectingNotificationId) {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(this.reconnectingNotificationId);
|
||||
this.reconnectingNotificationId = "";
|
||||
if (this.clearReconnectionAttemptMessage) {
|
||||
this.clearReconnectionAttemptMessage();
|
||||
this.clearReconnectionAttemptMessage = undefined;
|
||||
}
|
||||
const memoryUsageInfo = await response.json();
|
||||
if (memoryUsageInfo) {
|
||||
@@ -76,9 +75,8 @@ export class NotebookContainerClient {
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
|
||||
if (!this.reconnectingNotificationId) {
|
||||
this.reconnectingNotificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
if (!this.clearReconnectionAttemptMessage) {
|
||||
this.clearReconnectionAttemptMessage = logConsoleProgress(
|
||||
"Connection lost with Notebook server. Attempting to reconnect..."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,30 +2,35 @@
|
||||
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
||||
*/
|
||||
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { HttpStatusCodes, Areas } from "../../Common/Constants";
|
||||
import { GitHubReposPane } from "../Panes/GitHubReposPane";
|
||||
import type { ImmutableNotebook } from "@nteract/commutable";
|
||||
import type { IContentProvider } from "@nteract/core";
|
||||
import ko from "knockout";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { IContentProvider } from "@nteract/core";
|
||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
||||
import React from "react";
|
||||
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 { NotebookContentClient } from "./NotebookContentClient";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import Explorer from "../Explorer";
|
||||
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
||||
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||
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 {
|
||||
container: Explorer;
|
||||
@@ -49,7 +54,6 @@ export default class NotebookManager {
|
||||
|
||||
public gitHubReposPane: ContextualPaneBase;
|
||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
|
||||
|
||||
public initialize(params: NotebookManagerOptions): void {
|
||||
this.params = params;
|
||||
@@ -89,12 +93,6 @@ export default class NotebookManager {
|
||||
|
||||
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.gitHubClient.setToken(token?.access_token);
|
||||
|
||||
@@ -122,14 +120,25 @@ export default class NotebookManager {
|
||||
|
||||
public async openPublishNotebookPane(
|
||||
name: string,
|
||||
content: string | ImmutableNotebook,
|
||||
content: NotebookPaneContent,
|
||||
parentDomElement: HTMLElement
|
||||
): Promise<void> {
|
||||
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,10 +1,8 @@
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||
import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components";
|
||||
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
|
||||
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
@@ -14,7 +12,7 @@ import { AzureTheme } from "./AzureTheme";
|
||||
import "./base.css";
|
||||
import "./default.css";
|
||||
import "./NotebookReadOnlyRenderer.less";
|
||||
import IFrameOutputs from "./outputs/IFrameOutputs";
|
||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
contentRef: any;
|
||||
@@ -27,8 +25,10 @@ export interface NotebookRendererProps {
|
||||
*/
|
||||
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
componentDidMount() {
|
||||
if (!userContext.features.sandboxNotebookOutputs) {
|
||||
loadTransform(this.props as any);
|
||||
}
|
||||
}
|
||||
|
||||
private renderPrompt(id: string, contentRef: string): JSX.Element {
|
||||
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),
|
||||
outputs: userContext.features.sandboxNotebookOutputs
|
||||
? (props: any) => (
|
||||
<IFrameOutputs id={id} contentRef={contentRef}>
|
||||
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
||||
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
||||
<KernelOutputError />
|
||||
<StreamText />
|
||||
</IFrameOutputs>
|
||||
)
|
||||
? () => <SandboxOutputs id={id} contentRef={contentRef} />
|
||||
: undefined,
|
||||
editor: {
|
||||
monaco: (props: PassedEditorProps) =>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import { CellType } from "@nteract/commutable/src";
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
|
||||
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
@@ -23,7 +21,7 @@ import KeyboardShortcuts from "./decorators/kbd-shortcuts";
|
||||
import "./default.css";
|
||||
import MarkdownCell from "./markdown-cell";
|
||||
import "./NotebookRenderer.less";
|
||||
import IFrameOutputs from "./outputs/IFrameOutputs";
|
||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
||||
import Prompt from "./Prompt";
|
||||
import { promptContent } from "./PromptContent";
|
||||
import StatusBar from "./StatusBar";
|
||||
@@ -71,7 +69,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!userContext.features.sandboxNotebookOutputs) {
|
||||
loadTransform(this.props as any);
|
||||
}
|
||||
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} />,
|
||||
outputs: userContext.features.sandboxNotebookOutputs
|
||||
? (props: any) => (
|
||||
<IFrameOutputs id={id} contentRef={contentRef}>
|
||||
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
||||
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
||||
<KernelOutputError />
|
||||
<StreamText />
|
||||
</IFrameOutputs>
|
||||
)
|
||||
? () => <SandboxOutputs id={id} contentRef={contentRef} />
|
||||
: undefined,
|
||||
}}
|
||||
</CodeCell>
|
||||
|
||||
@@ -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);
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -0,0 +1,10 @@
|
||||
.shemaAnalyzerComponent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.schemaAnalyzerCard {
|
||||
max-width: 4096px;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as ko from "knockout";
|
||||
import { handleOpenAction } from "./OpenActions";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { ActionContracts } from "../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "./Explorer";
|
||||
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
import { handleOpenAction } from "./OpenActions";
|
||||
import AddCollectionPane from "./Panes/AddCollectionPane";
|
||||
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
|
||||
describe("OpenActions", () => {
|
||||
describe("handleOpenAction", () => {
|
||||
@@ -33,6 +33,7 @@ describe("OpenActions", () => {
|
||||
collection.expandCollection = jest.fn();
|
||||
collection.onDocumentDBDocumentsClick = jest.fn();
|
||||
collection.onMongoDBDocumentsClick = jest.fn();
|
||||
collection.onSchemaAnalyzerClick = jest.fn();
|
||||
collection.onTableEntitiesClick = jest.fn();
|
||||
collection.onGraphDocumentsClick = jest.fn();
|
||||
collection.onNewQueryClick = jest.fn();
|
||||
|
||||
@@ -79,6 +79,14 @@ function openCollectionTab(
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
) {
|
||||
collection.onSchemaAnalyzerClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
|
||||
@@ -331,7 +331,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
|
||||
if (currentCollections >= maxCollections) {
|
||||
let typeOfContainer = "collection";
|
||||
if (userContext.apiType === "Gremlin" || this.container.isPreferredApiTable()) {
|
||||
if (userContext.apiType === "Gremlin" || userContext.apiType === "Tables") {
|
||||
typeOfContainer = "container";
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.partitionKeyVisible = ko.computed<boolean>(() => {
|
||||
if (this.container == null || !!this.container.isPreferredApiTable()) {
|
||||
if (this.container == null || userContext.apiType === "Tables") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -757,7 +757,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!this.container.isPreferredApiTable()) {
|
||||
if (userContext.apiType === "Tables") {
|
||||
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
|
||||
this.databaseId(SharedConstants.CollectionCreation.TablesAPIDefaultDatabase);
|
||||
this.partitionKey("/'$pk'");
|
||||
@@ -917,8 +917,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
this.databaseId("");
|
||||
this.partitionKey("");
|
||||
this.throughputSpendAck(false);
|
||||
if (!this.container.isServerlessEnabled()) {
|
||||
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
|
||||
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
|
||||
}
|
||||
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
|
||||
@@ -952,7 +954,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
public isNonTableApi = (): boolean => {
|
||||
return !this.container.isPreferredApiTable();
|
||||
return userContext.apiType !== "Tables";
|
||||
};
|
||||
|
||||
public isUnlimitedStorageSelected = (): boolean => {
|
||||
@@ -1026,7 +1028,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
|
||||
private _setFocus() {
|
||||
// Autofocus is enabled on AddCollectionPane based on the preferred API
|
||||
if (this.container.isPreferredApiTable()) {
|
||||
if (userContext.apiType === "Tables") {
|
||||
const focusTableId = document.getElementById("containerId");
|
||||
focusTableId && focusTableId.focus();
|
||||
return;
|
||||
|
||||
@@ -26,7 +26,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getUpsellMessage } from "../../Utils/PricingUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
import Explorer from "../Explorer";
|
||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { AddDatabasePanel } from "./AddDatabasePanel";
|
||||
import { AddDatabasePanel } from "./AddDatabasePanelF";
|
||||
|
||||
const props = {
|
||||
explorer: new Explorer(),
|
||||
@@ -3,7 +3,7 @@ import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { Tooltip } from "../../../Common/Tooltip";
|
||||
import { Tooltip } from "../../../Common/Tooltip/Tooltip";
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { SubscriptionType } from "../../../Contracts/SubscriptionType";
|
||||
@@ -13,9 +13,12 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import Explorer from "../../Explorer";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
|
||||
import {
|
||||
GenericRightPaneComponent,
|
||||
GenericRightPaneProps,
|
||||
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
|
||||
export interface AddDatabasePaneProps {
|
||||
@@ -9,6 +9,48 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"_isAfecFeatureRegistered": [Function],
|
||||
"_isInitializingNotebooks": false,
|
||||
"_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 {
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
@@ -91,21 +133,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"useIndexingForSharedThroughput": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -126,42 +153,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"title": [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 {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
@@ -196,68 +187,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"title": [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 {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -319,20 +248,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"title": [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],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -419,43 +334,49 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"addCollectionText": [Function],
|
||||
"addDatabaseText": [Function],
|
||||
"addTableEntityPane": AddTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"addDatabasePane": AddDatabasePane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
"canExceedMaximumValue": [Function],
|
||||
"canRequestSupport": [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],
|
||||
"costsVisible": [Function],
|
||||
"databaseCreateNewShared": [Function],
|
||||
"databaseId": [Function],
|
||||
"databaseIdLabel": [Function],
|
||||
"databaseIdPlaceHolder": [Function],
|
||||
"databaseIdTooltipText": [Function],
|
||||
"databaseLevelThroughputTooltipText": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "addtableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isFreeTierAccount": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [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],
|
||||
},
|
||||
"addDatabaseText": [Function],
|
||||
"arcadiaToken": [Function],
|
||||
"canExceedMaximumValue": [Function],
|
||||
"canSaveQueries": [Function],
|
||||
@@ -532,21 +453,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"databaseAccount": [Function],
|
||||
"databases": [Function],
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionConfirmationPane": DeleteCollectionConfirmationPane {
|
||||
"collectionIdConfirmation": [Function],
|
||||
"collectionIdConfirmationText": [Function],
|
||||
"container": [Circular],
|
||||
"containerDeleteFeedback": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "deletecollectionconfirmationpane",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"recordDeleteFeedback": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
@@ -583,7 +489,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"flight": [Function],
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -605,10 +510,8 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"hasStorageAnalyticsAfecFeature": [Function],
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGitHubPaneEnabled": [Function],
|
||||
@@ -617,11 +520,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiCassandra": [Function],
|
||||
"isPreferredApiDocumentDB": [Function],
|
||||
"isPreferredApiGraph": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPreferredApiTable": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -632,22 +531,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
"isTabsContentExpanded": [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],
|
||||
"notebookServerInfo": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
@@ -659,27 +542,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"querySelectPane": QuerySelectPane {
|
||||
"allSelected": [Function],
|
||||
"anyColumnSelected": [Function],
|
||||
"availableColumnsTableQueryLabel": "Available Columns",
|
||||
"canSelectAll": [Function],
|
||||
"columnOptions": [Function],
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"handleClick": [Function],
|
||||
"id": "queryselectpane",
|
||||
"instructionLabel": "Select the columns that you want to query.",
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"noColumnSelectedWarning": "At least one column should be selected.",
|
||||
"selectedColumnOption": null,
|
||||
"title": [Function],
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -716,20 +578,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||
"setIsNotificationConsoleExpanded": 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],
|
||||
"sparkClusterConnectionInfo": [Function],
|
||||
"splitter": Splitter {
|
||||
@@ -758,32 +606,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
"title": [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 {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
@@ -887,10 +709,12 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
<div>
|
||||
<ThroughputInput
|
||||
isAutoscaleSelected={false}
|
||||
isDatabase={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
throughput={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,13 +4,13 @@ import React from "react";
|
||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
import { Query } from "../../../Contracts/DataModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import { BrowseQueriesPanel } from "./index";
|
||||
import { BrowseQueriesPane } from "./BrowseQueriesPane";
|
||||
|
||||
describe("Browse queries panel", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
|
||||
const fakeClientQuery = {} as QueriesClient;
|
||||
const fakeQueryData = {} as Query[];
|
||||
const fakeQueryData = [] as Query[];
|
||||
fakeClientQuery.getQueries = async () => fakeQueryData;
|
||||
fakeExplorer.queriesClient = fakeClientQuery;
|
||||
const props = {
|
||||
@@ -19,12 +19,12 @@ describe("Browse queries panel", () => {
|
||||
};
|
||||
|
||||
it("Should render Default properly", () => {
|
||||
const wrapper = mount(<BrowseQueriesPanel {...props} />);
|
||||
const wrapper = mount(<BrowseQueriesPane {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -13,15 +13,15 @@ import {
|
||||
import Explorer from "../../Explorer";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
|
||||
interface BrowseQueriesPanelProps {
|
||||
interface BrowseQueriesPaneProps {
|
||||
explorer: Explorer;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export const BrowseQueriesPanel: FunctionComponent<BrowseQueriesPanelProps> = ({
|
||||
export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
|
||||
explorer,
|
||||
closePanel,
|
||||
}: BrowseQueriesPanelProps): JSX.Element => {
|
||||
}: BrowseQueriesPaneProps): JSX.Element => {
|
||||
const loadSavedQuery = (savedQuery: Query): void => {
|
||||
const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
|
||||
if (!selectedCollection) {
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Browse queries panel Should render Default properly 1`] = `
|
||||
<BrowseQueriesPanel
|
||||
<BrowseQueriesPane
|
||||
closePanel={[Function]}
|
||||
explorer={
|
||||
Object {
|
||||
@@ -54,5 +54,5 @@ exports[`Browse queries panel Should render Default properly 1`] = `
|
||||
</QueriesGridComponent>
|
||||
</div>
|
||||
</div>
|
||||
</BrowseQueriesPanel>
|
||||
</BrowseQueriesPane>
|
||||
`;
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
156
src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx
Normal file
156
src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 {
|
||||
Stack,
|
||||
Label,
|
||||
Text,
|
||||
Dropdown,
|
||||
IDropdownProps,
|
||||
IDropdownOption,
|
||||
SelectableOptionMenuItemType,
|
||||
IDropdownProps,
|
||||
IRenderFunction,
|
||||
ISelectableOption,
|
||||
Label,
|
||||
SelectableOptionMenuItemType,
|
||||
Stack,
|
||||
Text,
|
||||
} 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 {
|
||||
type: "MyNotebooks" | "GitHub";
|
||||
@@ -26,46 +26,25 @@ interface Location {
|
||||
export interface CopyNotebookPaneProps {
|
||||
name: string;
|
||||
pinnedRepos: IPinnedRepo[];
|
||||
onDropDownChange: (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
|
||||
onDropDownChange: (_: FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
|
||||
}
|
||||
|
||||
export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneProps> {
|
||||
private static readonly BranchNameWhiteSpace = " ";
|
||||
export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps> = ({
|
||||
name,
|
||||
pinnedRepos,
|
||||
onDropDownChange,
|
||||
}: CopyNotebookPaneProps) => {
|
||||
const BranchNameWhiteSpace = " ";
|
||||
|
||||
public render(): 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 => {
|
||||
const onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
||||
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>;
|
||||
};
|
||||
|
||||
private getDropDownOptions = (): IDropdownOption[] => {
|
||||
const getDropDownOptions = (): IDropdownOption[] => {
|
||||
const options: IDropdownOption[] = [];
|
||||
|
||||
options.push({
|
||||
@@ -77,7 +56,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
} as Location,
|
||||
});
|
||||
|
||||
if (this.props.pinnedRepos && this.props.pinnedRepos.length > 0) {
|
||||
if (pinnedRepos && pinnedRepos.length > 0) {
|
||||
options.push({
|
||||
key: "GitHub-Header-Divider",
|
||||
text: undefined,
|
||||
@@ -90,7 +69,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
itemType: SelectableOptionMenuItemType.Header,
|
||||
});
|
||||
|
||||
this.props.pinnedRepos.forEach((pinnedRepo) => {
|
||||
pinnedRepos.forEach((pinnedRepo) => {
|
||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||
options.push({
|
||||
key: `GitHub-Repo-${repoFullName}`,
|
||||
@@ -101,7 +80,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
pinnedRepo.branches.forEach((branch) =>
|
||||
options.push({
|
||||
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
|
||||
text: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`,
|
||||
text: `${BranchNameWhiteSpace}${branch.name}`,
|
||||
title: `${repoFullName} - ${branch.name}`,
|
||||
data: {
|
||||
type: "GitHub",
|
||||
@@ -116,4 +95,26 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,6 @@ jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import { DeleteCollectionConfirmationPanel } from ".";
|
||||
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
|
||||
import DeleteFeedback from "../../../Common/DeleteFeedback";
|
||||
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 { updateUserContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
|
||||
|
||||
describe("Delete Collection Confirmation Pane", () => {
|
||||
describe("Explorer.isLastCollection()", () => {
|
||||
@@ -65,7 +65,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
closePanel: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
|
||||
props.explorer.isLastCollection = () => true;
|
||||
@@ -119,7 +119,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
closePanel: (): void => undefined,
|
||||
collectionName: "container",
|
||||
};
|
||||
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
wrapper = mount(<DeleteCollectionConfirmationPane {...props} />);
|
||||
});
|
||||
|
||||
it("should call delete collection", () => {
|
||||
@@ -11,18 +11,21 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
|
||||
export interface DeleteCollectionConfirmationPanelProps {
|
||||
import {
|
||||
GenericRightPaneComponent,
|
||||
GenericRightPaneProps,
|
||||
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||
export interface DeleteCollectionConfirmationPaneProps {
|
||||
explorer: Explorer;
|
||||
collectionName: string;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export const DeleteCollectionConfirmationPanel: FunctionComponent<DeleteCollectionConfirmationPanelProps> = ({
|
||||
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
||||
explorer,
|
||||
closePanel,
|
||||
collectionName,
|
||||
}: DeleteCollectionConfirmationPanelProps) => {
|
||||
}: DeleteCollectionConfirmationPaneProps) => {
|
||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||
const [inputCollectionName, setInputCollectionName] = useState<string>("");
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
|
||||
<DeleteCollectionConfirmationPanel
|
||||
<DeleteCollectionConfirmationPane
|
||||
closePanel={[Function]}
|
||||
collectionName="container"
|
||||
explorer={
|
||||
@@ -3627,5 +3627,5 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
</div>
|
||||
</div>
|
||||
</GenericRightPaneComponent>
|
||||
</DeleteCollectionConfirmationPanel>
|
||||
</DeleteCollectionConfirmationPane>
|
||||
`;
|
||||
@@ -2,7 +2,7 @@ import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import { ExecuteSprocParamsPanel } from "./index";
|
||||
import { ExecuteSprocParamsPane } from "./ExecuteSprocParamsPane";
|
||||
|
||||
describe("Excute Sproc Param Pane", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
@@ -14,23 +14,23 @@ describe("Excute Sproc Param Pane", () => {
|
||||
};
|
||||
|
||||
it("should render Default properly", () => {
|
||||
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
|
||||
const wrapper = mount(<ExecuteSprocParamsPane {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("add a new parameter field", () => {
|
||||
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
|
||||
const wrapper = mount(<ExecuteSprocParamsPane {...props} />);
|
||||
wrapper.find("#addparam").last().simulate("click");
|
||||
expect(wrapper.find("input[type='text']")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("remove a parameter field", () => {
|
||||
const wrapper = mount(<ExecuteSprocParamsPanel {...props} />);
|
||||
const wrapper = mount(<ExecuteSprocParamsPane {...props} />);
|
||||
wrapper.find("#deleteparam").last().simulate("click");
|
||||
expect(wrapper.find("input[type='text']")).toHaveLength(1);
|
||||
});
|
||||
@@ -4,7 +4,10 @@ import React, { FunctionComponent, useState } from "react";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import Explorer from "../../Explorer";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
|
||||
import {
|
||||
GenericRightPaneComponent,
|
||||
GenericRightPaneProps,
|
||||
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||
import { InputParameter } from "./InputParameter";
|
||||
|
||||
interface ExecuteSprocParamsPaneProps {
|
||||
@@ -23,7 +26,7 @@ interface UnwrappedExecuteSprocParam {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
|
||||
export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
|
||||
explorer,
|
||||
storedProcedure,
|
||||
closePanel,
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
<ExecuteSprocParamsPanel
|
||||
<ExecuteSprocParamsPane
|
||||
closePanel={[Function]}
|
||||
explorer={Object {}}
|
||||
storedProcedure={Object {}}
|
||||
@@ -1062,7 +1062,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<button
|
||||
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}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -1075,16 +1075,16 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-73"
|
||||
className="ms-Button-flexContainer flexContainer-41"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<Component
|
||||
className="ms-Button-icon icon-75"
|
||||
className="ms-Button-icon icon-43"
|
||||
iconName="Cancel"
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="presentation"
|
||||
style={
|
||||
@@ -1429,7 +1429,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="ms-Label root-81"
|
||||
className="ms-Label root-49"
|
||||
>
|
||||
Partition key value
|
||||
</label>
|
||||
@@ -1439,7 +1439,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
>
|
||||
<div
|
||||
className="ms-Stack css-82"
|
||||
className="ms-Stack css-50"
|
||||
>
|
||||
<StyledWithResponsiveMode
|
||||
key=".0:$.0"
|
||||
@@ -2336,7 +2336,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="ms-Label ms-Dropdown-label root-99"
|
||||
className="ms-Label ms-Dropdown-label root-67"
|
||||
id="Dropdown3-label"
|
||||
>
|
||||
Key
|
||||
@@ -2348,7 +2348,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="Dropdown3-label Dropdown3-option"
|
||||
className="ms-Dropdown dropdown-83"
|
||||
className="ms-Dropdown dropdown-51"
|
||||
data-is-focusable={true}
|
||||
id="Dropdown3"
|
||||
onBlur={[Function]}
|
||||
@@ -2368,23 +2368,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
aria-posinset={1}
|
||||
aria-selected={true}
|
||||
aria-setsize={2}
|
||||
className="ms-Dropdown-title title-84"
|
||||
className="ms-Dropdown-title title-52"
|
||||
id="Dropdown3-option"
|
||||
role="option"
|
||||
>
|
||||
String
|
||||
</span>
|
||||
<span
|
||||
className="ms-Dropdown-caretDownWrapper caretDownWrapper-85"
|
||||
className="ms-Dropdown-caretDownWrapper caretDownWrapper-53"
|
||||
>
|
||||
<StyledIconBase
|
||||
aria-hidden={true}
|
||||
className="ms-Dropdown-caretDown caretDown-86"
|
||||
className="ms-Dropdown-caretDown caretDown-54"
|
||||
iconName="ChevronDown"
|
||||
>
|
||||
<IconBase
|
||||
aria-hidden={true}
|
||||
className="ms-Dropdown-caretDown caretDown-86"
|
||||
className="ms-Dropdown-caretDown caretDown-54"
|
||||
iconName="ChevronDown"
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -2663,7 +2663,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className="ms-Dropdown-caretDown caretDown-100"
|
||||
className="ms-Dropdown-caretDown caretDown-68"
|
||||
data-icon-name="ChevronDown"
|
||||
>
|
||||
|
||||
@@ -2969,7 +2969,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
validateOnLoad={true}
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-102"
|
||||
className="ms-TextField root-70"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
@@ -3258,7 +3258,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="ms-Label root-81"
|
||||
className="ms-Label root-49"
|
||||
htmlFor="confirmCollectionId"
|
||||
id="TextFieldLabel6"
|
||||
>
|
||||
@@ -3267,13 +3267,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</LabelBase>
|
||||
</StyledLabelBase>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-103"
|
||||
className="ms-TextField-fieldGroup fieldGroup-71"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-labelledby="TextFieldLabel6"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-104"
|
||||
className="ms-TextField-field field-72"
|
||||
id="confirmCollectionId"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -3581,7 +3581,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="ms-Label root-81"
|
||||
className="ms-Label root-49"
|
||||
>
|
||||
Enter input parameters (if any)
|
||||
</label>
|
||||
@@ -3591,7 +3591,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
>
|
||||
<div
|
||||
className="ms-Stack css-82"
|
||||
className="ms-Stack css-50"
|
||||
>
|
||||
<StyledWithResponsiveMode
|
||||
key=".0:$.0"
|
||||
@@ -4488,7 +4488,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="ms-Label ms-Dropdown-label root-99"
|
||||
className="ms-Label ms-Dropdown-label root-67"
|
||||
id="Dropdown7-label"
|
||||
>
|
||||
Key
|
||||
@@ -4500,7 +4500,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="Dropdown7-label Dropdown7-option"
|
||||
className="ms-Dropdown dropdown-83"
|
||||
className="ms-Dropdown dropdown-51"
|
||||
data-is-focusable={true}
|
||||
id="Dropdown7"
|
||||
onBlur={[Function]}
|
||||
@@ -4520,23 +4520,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
aria-posinset={1}
|
||||
aria-selected={true}
|
||||
aria-setsize={2}
|
||||
className="ms-Dropdown-title title-84"
|
||||
className="ms-Dropdown-title title-52"
|
||||
id="Dropdown7-option"
|
||||
role="option"
|
||||
>
|
||||
String
|
||||
</span>
|
||||
<span
|
||||
className="ms-Dropdown-caretDownWrapper caretDownWrapper-85"
|
||||
className="ms-Dropdown-caretDownWrapper caretDownWrapper-53"
|
||||
>
|
||||
<StyledIconBase
|
||||
aria-hidden={true}
|
||||
className="ms-Dropdown-caretDown caretDown-86"
|
||||
className="ms-Dropdown-caretDown caretDown-54"
|
||||
iconName="ChevronDown"
|
||||
>
|
||||
<IconBase
|
||||
aria-hidden={true}
|
||||
className="ms-Dropdown-caretDown caretDown-86"
|
||||
className="ms-Dropdown-caretDown caretDown-54"
|
||||
iconName="ChevronDown"
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -4815,7 +4815,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className="ms-Dropdown-caretDown caretDown-100"
|
||||
className="ms-Dropdown-caretDown caretDown-68"
|
||||
data-icon-name="ChevronDown"
|
||||
>
|
||||
|
||||
@@ -5123,7 +5123,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
value=""
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-102"
|
||||
className="ms-TextField root-70"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
@@ -5412,7 +5412,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="ms-Label root-81"
|
||||
className="ms-Label root-49"
|
||||
htmlFor="confirmCollectionId"
|
||||
id="TextFieldLabel10"
|
||||
>
|
||||
@@ -5421,13 +5421,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</LabelBase>
|
||||
</StyledLabelBase>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-103"
|
||||
className="ms-TextField-fieldGroup fieldGroup-71"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-labelledby="TextFieldLabel10"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-104"
|
||||
className="ms-TextField-field field-72"
|
||||
id="confirmCollectionId"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -5735,7 +5735,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
width={20}
|
||||
>
|
||||
<div
|
||||
className="ms-Image addRemoveIconLabel root-113"
|
||||
className="ms-Image addRemoveIconLabel root-81"
|
||||
style={
|
||||
Object {
|
||||
"height": 30,
|
||||
@@ -5745,7 +5745,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<img
|
||||
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"
|
||||
key="fabricImage"
|
||||
onClick={[Function]}
|
||||
@@ -6050,7 +6050,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
width={20}
|
||||
>
|
||||
<div
|
||||
className="ms-Image addRemoveIconLabel root-113"
|
||||
className="ms-Image addRemoveIconLabel root-81"
|
||||
style={
|
||||
Object {
|
||||
"height": 30,
|
||||
@@ -6060,7 +6060,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<img
|
||||
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"
|
||||
key="fabricImage"
|
||||
onClick={[Function]}
|
||||
@@ -6079,7 +6079,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="ms-Stack css-82"
|
||||
className="ms-Stack css-50"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<StyledImageBase
|
||||
@@ -6371,7 +6371,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
width={20}
|
||||
>
|
||||
<div
|
||||
className="ms-Image root-113"
|
||||
className="ms-Image root-81"
|
||||
style={
|
||||
Object {
|
||||
"height": 30,
|
||||
@@ -6381,7 +6381,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<img
|
||||
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"
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
@@ -6395,7 +6395,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
key=".0:$.1"
|
||||
>
|
||||
<span
|
||||
className="addNewParamStyle css-115"
|
||||
className="addNewParamStyle css-83"
|
||||
>
|
||||
Add New Param
|
||||
</span>
|
||||
@@ -8121,7 +8121,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
>
|
||||
<button
|
||||
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}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
@@ -8139,14 +8139,14 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-73"
|
||||
className="ms-Button-flexContainer flexContainer-41"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-textContainer textContainer-74"
|
||||
className="ms-Button-textContainer textContainer-42"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-label label-117"
|
||||
className="ms-Button-label label-85"
|
||||
id="id__11"
|
||||
key="id__11"
|
||||
>
|
||||
@@ -8176,5 +8176,5 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</GenericRightPaneComponent>
|
||||
</ExecuteSprocParamsPanel>
|
||||
</ExecuteSprocParamsPane>
|
||||
`;
|
||||
@@ -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>
|
||||
@@ -1,10 +0,0 @@
|
||||
@import "../../../less/Common/Constants";
|
||||
|
||||
.newvertexContainer {
|
||||
height:100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { LoadQueryPanel } from "./index";
|
||||
import { LoadQueryPane } from "./LoadQueryPane";
|
||||
|
||||
describe("Load Query Pane", () => {
|
||||
it("should render Default properly", () => {
|
||||
@@ -11,7 +11,7 @@ describe("Load Query Pane", () => {
|
||||
closePanel: (): void => undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<LoadQueryPanel {...props} />);
|
||||
const wrapper = shallow(<LoadQueryPane {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -3,22 +3,25 @@ import { IImageProps, Image, ImageFit, Stack, TextField } from "office-ui-fabric
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import folderIcon from "../../../../images/folder_16x16.svg";
|
||||
import { logError } from "../../../Common/Logger";
|
||||
import { Collection } from "../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import QueryTab from "../../Tabs/QueryTab";
|
||||
import { Collection } from "..//../../Contracts/ViewModels";
|
||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
|
||||
import {
|
||||
GenericRightPaneComponent,
|
||||
GenericRightPaneProps,
|
||||
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||
|
||||
interface LoadQueryPanelProps {
|
||||
interface LoadQueryPaneProps {
|
||||
explorer: Explorer;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export const LoadQueryPanel: FunctionComponent<LoadQueryPanelProps> = ({
|
||||
export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
|
||||
explorer,
|
||||
closePanel,
|
||||
}: LoadQueryPanelProps): JSX.Element => {
|
||||
}: LoadQueryPaneProps): JSX.Element => {
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
|
||||
@@ -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}`;
|
||||
};
|
||||
}
|
||||
10
src/Explorer/Panes/NewVertexPanel/NewVertexPanel.less
Normal file
10
src/Explorer/Panes/NewVertexPanel/NewVertexPanel.less
Normal 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
Reference in New Issue
Block a user