Compare commits

..

1 Commits

Author SHA1 Message Date
Armando Trejo Oliver
af25fad216 Fix table api query projections (#584)
When building queries with projections, the resulting query does not include the "id" property as part of the projection. The "id" property is used by the results grid to display as the RowKey so the result is queries wih projections do not show the RowKey.

This change fixes this by including "id" as part of the projections.
2021-03-26 12:30:37 -07:00
215 changed files with 11648 additions and 28728 deletions

View File

@@ -97,6 +97,7 @@ src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
src/Explorer/Menus/ContextMenu.ts
src/Explorer/MostRecentActivity/MostRecentActivity.ts
src/Explorer/Notebook/FileSystemUtil.ts
src/Explorer/Notebook/NotebookClientV2.ts
src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts
src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts
@@ -125,20 +126,29 @@ src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/ExecuteSprocParamsPane.ts
src/Explorer/Panes/GraphStylingPane.ts
src/Explorer/Panes/LoadQueryPane.ts
src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SaveQueryPane.ts
src/Explorer/Panes/SettingsPane.test.ts
src/Explorer/Panes/SettingsPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/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/QuerySelectPane.ts
src/Explorer/Panes/Tables/TableColumnOptionsPane.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/UploadFilePane.ts
src/Explorer/Panes/UploadItemsPane.ts
src/Explorer/SplashScreen/SplashScreen.test.ts
src/Explorer/Tables/Constants.ts
src/Explorer/Tables/DataTable/CacheBase.ts
@@ -245,8 +255,11 @@ src/Terminal/NotebookAppContracts.d.ts
src/Terminal/index.ts
src/TokenProviders/PortalTokenProvider.ts
src/TokenProviders/TokenProviderFactory.ts
src/Utils/DatabaseAccountUtils.test.ts
src/Utils/DatabaseAccountUtils.ts
src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts
src/applyExplorerBindings.ts
src/global.d.ts
src/setupTests.ts
@@ -298,6 +311,8 @@ src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.t
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx

View File

@@ -3,7 +3,7 @@ module.exports = {
browser: true,
es6: true,
},
plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks"],
plugins: ["@typescript-eslint", "no-null", "prefer-arrow"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: {
Atomics: "readonly",
@@ -20,7 +20,7 @@ module.exports = {
overrides: [
{
files: ["**/*.tsx"],
extends: ["plugin:react/recommended"],
extends: ["plugin:react/recommended"], // TODO: Add react-hooks
plugins: ["react"],
},
{
@@ -42,8 +42,6 @@ module.exports = {
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error",
"react/display-name": "off",
"react-hooks/rules-of-hooks": "warn", // TODO: error
"react-hooks/exhaustive-deps": "warn", // TODO: error
"no-restricted-syntax": [
"error",
{

View File

@@ -1 +0,0 @@
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)

View File

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

View File

@@ -70,6 +70,7 @@ jobs:
- run: npm run test
build:
runs-on: ubuntu-latest
needs: [lint, format, compile, unittest]
name: "Build"
steps:
- uses: actions/checkout@v2
@@ -91,14 +92,6 @@ jobs:
with:
name: dist
path: dist/
- name: Upload build to preview blob storage
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --destination-path "${{github.event.pull_request.head.sha}}" --account-key="${PREVIEW_STORAGE_KEY}"
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
- name: Upload preview config to blob storage
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --name "${{github.event.pull_request.head.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
env:
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/')

View File

@@ -18,6 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
### Hosted Development (https://cosmos.azure.com)
- Visit: `https://localhost:1234/hostedExplorer.html`
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
### Emulator Development

View File

@@ -67,8 +67,7 @@ module.exports = {
// A map from regular expressions to module names that allow to stub out resources with a single module
moduleNameMapper: {
"^.*[.](svg|png|gif|less|css)$": "<rootDir>/mockModule",
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
"^.*[.](svg|png|gif|less)$": "<rootDir>/mockModule",
"worker-loader": "<rootDir>/mockModule",
"office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1", // https://github.com/OfficeDev/office-ui-fabric-react/wiki/Fabric-6-Release-Notes
"^dnd-core$": "dnd-core/dist/cjs",

View File

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

7182
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@
"@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1",
"@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.4.2",
"@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0",
@@ -25,12 +25,12 @@
"@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0",
"@nteract/markdown": "4.6.1",
"@nteract/markdown": "4.4.0",
"@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7",
"@nteract/stateful-components": "1.7.7",
"@nteract/stateful-components": "1.7.0",
"@nteract/styles": "2.0.2",
"@nteract/transform-geojson": "5.1.8",
"@nteract/transform-model-debug": "5.0.1",
@@ -44,7 +44,9 @@
"@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110",
"@uifabric/styling": "7.13.7",
"abort-controller": "3.0.0",
"applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",
"clean-webpack-plugin": "0.1.19",
@@ -58,6 +60,8 @@
"date-fns": "1.29.0",
"dayjs": "1.8.19",
"dotenv": "8.2.0",
"es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0",
"hasher": "1.2.0",
@@ -75,9 +79,12 @@
"monaco-editor": "0.18.1",
"ms": "2.1.3",
"msal": "1.4.4",
"object.entries": "1.1.0",
"office-ui-fabric-react": "7.164.2",
"p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3",
"promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0",
"q": "1.5.1",
"react": "16.13.1",
"react-animate-height": "2.0.8",
@@ -94,9 +101,13 @@
"rxjs": "6.6.3",
"styled-components": "4.3.2",
"swr": "0.4.0",
"terser-webpack-plugin": "3.1.0",
"text-encoding": "0.7.0",
"underscore": "1.9.1",
"utility-types": "3.10.0"
"url-polyfill": "1.1.7",
"utility-types": "3.10.0",
"webcrypto-liner": "1.1.4",
"webfontloader": "1.6.28",
"whatwg-fetch": "3.0.0"
},
"devDependencies": {
"@babel/core": "7.9.0",
@@ -126,7 +137,9 @@
"@types/react-redux": "7.1.7",
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1",
"@types/text-encoding": "0.0.33",
"@types/underscore": "1.7.36",
"@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1",
"axe-puppeteer": "1.1.0",
@@ -151,6 +164,7 @@
"html-loader": "0.5.5",
"html-loader-jest": "0.2.1",
"html-webpack-plugin": "3.2.0",
"inline-css": "2.2.5",
"jest": "25.5.4",
"jest-canvas-mock": "2.1.0",
"jest-puppeteer": "4.4.0",
@@ -167,10 +181,11 @@
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
"terser-webpack-plugin": "3.0.5",
"ts-loader": "6.2.2",
"tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0",
"typescript": "4.2.3",
"typescript": "4.0.2",
"url-loader": "1.1.1",
"wait-on": "4.0.2",
"webpack": "4.43.0",
@@ -200,8 +215,8 @@
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts",
"strict:find": "node ./strict-null-checks/find.js",
"strict:add": "node ./strict-null-checks/auto-add.js",
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
"autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
},
@@ -223,4 +238,4 @@
"prettier": {
"printWidth": 120
}
}
}

View File

@@ -1,7 +0,0 @@
[defaults]
group = stfaul
sku = P1v2
appserviceplan = stfaul_asp_Linux_centralus_0
location = centralus
web = cosmos-explorer-preview

View File

@@ -1,20 +0,0 @@
# Cosmos Explorer Preview
Cosmos Explorer Preview makes it possible to try a working version of any commit on master or in a PR. No need to run the app locally or deploy to staging.
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
Connection string URLs: https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.
### Architechture
- This folder contains a NodeJS app deployed to Azure App Service that powers preview URLs:
- Paths starting with `/commit/` are proxied to an Azure Storage account containing build artifacts
- Paths starting with `/proxy/` are proxied dynamically to Cosmos account endpoints. Required otherwise CORS would need to be configured for every account accessed.
- Paths starting with `/api/` are proxied to Portal APIs that do not support CORS.
- On GitHub Actions build completion:
- All files in dist are uploaded to an Azure Storage account namespaced by the SHA of the commit
- `/preview/config.json` is uploaded to the same folder with preview specific configuration

View File

@@ -1,3 +0,0 @@
{
"PROXY_PATH": "/proxy"
}

View File

@@ -1,70 +0,0 @@
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const port = process.env.PORT || 3000;
const fetch = require("node-fetch");
const api = createProxyMiddleware("/api", {
target: "https://main.documentdb.ext.azure.com",
changeOrigin: true,
logLevel: "debug",
bypass: (req, res) => {
if (req.method === "OPTIONS") {
res.statusCode = 200;
res.send();
}
},
});
const proxy = createProxyMiddleware("/proxy", {
target: "https://main.documentdb.ext.azure.com",
changeOrigin: true,
secure: false,
logLevel: "debug",
pathRewrite: { "^/proxy": "" },
router: (req) => {
let newTarget = req.headers["x-ms-proxy-target"];
return newTarget;
},
});
const commit = createProxyMiddleware("/commit", {
target: "https://cosmosexplorerpreview.blob.core.windows.net",
changeOrigin: true,
secure: false,
logLevel: "debug",
pathRewrite: { "^/commit": "$web/" },
});
const app = express();
app.use(api);
app.use(proxy);
app.use(commit);
app.get("/pull/:pr(\\d+)", (req, res) => {
const pr = req.params.pr;
const [, query] = req.originalUrl.split("?");
const search = new URLSearchParams(query);
fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
.then((response) => response.json())
.then(({ head: { ref, sha } }) => {
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
prUrl.hash = ref;
search.set("feature.pr", prUrl.href);
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
explorer.search = search.toString();
const portal = new URL("https://ms.portal.azure.com/");
portal.searchParams.set("dataExplorerSource", explorer.href);
portal.hash =
"@microsoft.onmicrosoft.com/resource/subscriptions/b9c77f10-b438-4c32-9819-eef8a654e478/resourceGroups/stfaul/providers/Microsoft.DocumentDb/databaseAccounts/stfaul-sql/dataExplorer";
return res.redirect(portal.href);
})
.catch(() => res.sendStatus(500));
});
app.listen(port, () => {
console.log(`Example app listening on port: ${port}`);
});

1146
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"name": "cosmos-explorer-preview",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"deploy": "az webapp up -n cosmos-explorer-preview --subscription cosmosdb-portalteam-generaldemo -g stfaul",
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Microsoft Corporation",
"dependencies": {
"express": "^4.17.1",
"http-proxy-middleware": "^1.1.0",
"node-fetch": "^2.6.1"
}
}

View File

@@ -98,6 +98,31 @@ export class CapabilityNames {
public static readonly EnableServerless: string = "EnableServerless";
}
export class Features {
public static readonly cosmosdb = "cosmosdb";
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
public static readonly executeSproc = "dataexplorerexecutesproc";
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl";
public static readonly notebookServerToken = "notebookservertoken";
public static readonly notebookBasePath = "notebookbasepath";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey";
public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1";
public static readonly selfServeType = "selfservetype";
public static readonly enableKOPanel = "enablekopanel";
public static readonly enableReactPane = "enablereactpane";
}
// flight names returned from the portal are always lowercase
export class Flights {
public static readonly SettingsV2 = "settingsv2";

View File

@@ -32,7 +32,7 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
};
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
requestContext.endpoint = configContext.PROXY_PATH;
requestContext.headers["x-ms-proxy-target"] = endpoint();
return next(requestContext);
};

View File

@@ -1,7 +1,7 @@
import { SeverityLevel } from "@microsoft/applicationinsights-web";
import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts";
import { trackTrace } from "../Shared/appInsights";
import { sendMessage } from "./MessageHandler";
import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts";
import { appInsights } from "../Shared/appInsights";
import { SeverityLevel } from "@microsoft/applicationinsights-web";
// TODO: Move to a separate Diagnostics folder
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -46,7 +46,7 @@ function _logEntry(entry: Diagnostics.LogEntry): void {
return SeverityLevel.Information;
}
})(entry.level);
trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
}
function _generateLogEntry(

View File

@@ -48,18 +48,32 @@ export function sendCachedDataMessage<TResponseDataModel>(
}
export function sendMessage(data: any): void {
_sendMessage({
signature: "pcIframe",
data: data,
});
if (canSendMessage()) {
// We try to find data explorer window first, then fallback to current window
const portalChildWindow = getDataExplorerWindow(window) || window;
portalChildWindow.parent.postMessage(
{
signature: "pcIframe",
data: data,
},
portalChildWindow.document.referrer
);
}
}
export function sendReadyMessage(): void {
_sendMessage({
signature: "pcIframe",
kind: "ready",
data: "ready",
});
if (canSendMessage()) {
// We try to find data explorer window first, then fallback to current window
const portalChildWindow = getDataExplorerWindow(window) || window;
portalChildWindow.parent.postMessage(
{
signature: "pcIframe",
kind: "ready",
data: "ready",
},
portalChildWindow.document.referrer
);
}
}
export function canSendMessage(): boolean {
@@ -75,17 +89,3 @@ export function runGarbageCollector() {
}
});
}
const _sendMessage = (message: any): void => {
if (canSendMessage()) {
// Portal window can receive messages from only child windows
const portalChildWindow = getDataExplorerWindow(window) || window;
if (portalChildWindow === window) {
// Current window is a child of portal, send message to portal window
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*");
} else {
// Current window is not a child of portal, send message to the child window instead (which is data explorer)
portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*");
}
}
};

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
exports[`requestPlugin Emulator builds a url for emulator proxy via webpack 1`] = `
Object {
"endpoint": "http://localhost/proxy",
"endpoint": "/proxy",
"headers": Object {
"x-ms-proxy-target": "http://localhost",
},
@@ -12,7 +12,7 @@ Object {
exports[`requestPlugin Hosted builds a proxy URL in development 1`] = `
Object {
"endpoint": "http://localhost/proxy",
"endpoint": "/proxy",
"headers": Object {
"x-ms-proxy-target": "baz",
},

View File

@@ -1,15 +1,15 @@
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { userContext } from "../../UserContext";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
@@ -17,6 +17,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if (
userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
return await readCollectionsWithARM(databaseId);

View File

@@ -1,37 +1,39 @@
import { ContainerDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { userContext } from "../../UserContext";
import {
CreateUpdateOptions,
ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
MongoDBCollectionResource,
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import {
createUpdateCassandraTable,
getCassandraTable,
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateGremlinGraph,
getGremlinGraph,
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import {
ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
createUpdateGremlinGraph,
getGremlinGraph,
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function updateCollection(
databaseId: string,
collectionId: string,
newCollection: Partial<Collection>,
newCollection: Collection,
options: RequestOptions = {}
): Promise<Collection> {
let collection: Collection;
@@ -41,6 +43,7 @@ export async function updateCollection(
if (
userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
@@ -66,7 +69,7 @@ export async function updateCollection(
async function updateCollectionWithARM(
databaseId: string,
collectionId: string,
newCollection: Partial<Collection>
newCollection: Collection
): Promise<Collection> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
@@ -82,15 +85,6 @@ async function updateCollectionWithARM(
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Table:
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
@@ -102,7 +96,7 @@ async function updateSqlContainer(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Partial<Collection>
newCollection: Collection
): Promise<Collection> {
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -121,26 +115,35 @@ async function updateSqlContainer(
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
}
export async function updateMongoDBCollection(
export async function updateMongoDBCollectionThroughRP(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Partial<Collection>
): Promise<Collection> {
newCollection: MongoDBCollectionResource,
updateOptions?: CreateUpdateOptions
): Promise<MongoDBCollectionResource> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateParams: MongoDBCollectionCreateUpdateParameters = {
properties: {
resource: newCollection,
options: updateOptions,
},
};
const updateResponse = await createUpdateMongoDBCollection(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as MongoDBCollectionCreateUpdateParameters
updateParams
);
return updateResponse && (updateResponse.properties.resource as Collection);
return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource);
}
throw new Error(
@@ -154,7 +157,7 @@ async function updateCassandraTable(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Partial<Collection>
newCollection: Collection
): Promise<Collection> {
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -181,7 +184,7 @@ async function updateGremlinGraph(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Partial<Collection>
newCollection: Collection
): Promise<Collection> {
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -205,7 +208,7 @@ async function updateTable(
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Partial<Collection>
newCollection: Collection
): Promise<Collection> {
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {

View File

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

View File

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

View File

@@ -1,29 +1,16 @@
import * as ko from "knockout";
import * as PaneComponents from "./Panes/PaneComponents";
import * as TabComponents from "./Tabs/TabComponents";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
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";
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent);
@@ -34,26 +21,28 @@ 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 });
ko.components.register("tabs-manager", TabsManagerKOComponent());
// 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 }));
ko.components.register("documents-tab", new TabComponents.DocumentsTab());
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
ko.components.register("collection-settings-tab-v2", new TabComponents.SettingsTabV2());
ko.components.register("query-tab", new TabComponents.QueryTab());
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
ko.components.register("graph-tab", new TabComponents.GraphTab());
ko.components.register("mongo-shell-tab", new TabComponents.MongoShellTab());
ko.components.register("conflicts-tab", new TabComponents.ConflictsTab());
ko.components.register("notebookv2-tab", new TabComponents.NotebookV2Tab());
ko.components.register("terminal-tab", new TabComponents.TerminalTab());
ko.components.register("gallery-tab", new TabComponents.GalleryTab());
ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTab());
// Database Tabs
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
ko.components.register("database-settings-tab-v2", new TabComponents.SettingsTabV2());
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
@@ -62,12 +51,24 @@ ko.components.register(
"delete-collection-confirmation-pane",
new PaneComponents.DeleteCollectionConfirmationPaneComponent()
);
ko.components.register(
"delete-database-confirmation-pane",
new PaneComponents.DeleteDatabaseConfirmationPaneComponent()
);
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("table-column-options-pane", new PaneComponents.TableColumnOptionsPaneComponent());
ko.components.register("table-query-select-pane", new PaneComponents.TableQuerySelectPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("settings-pane", new PaneComponents.SettingsPaneComponent());
ko.components.register("execute-sproc-params-pane", new PaneComponents.ExecuteSprocParamsComponent());
ko.components.register("upload-items-pane", new PaneComponents.UploadItemsPaneComponent());
ko.components.register("load-query-pane", new PaneComponents.LoadQueryPaneComponent());
ko.components.register("save-query-pane", new PaneComponents.SaveQueryPaneComponent());
ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPaneComponent());
ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

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

View File

@@ -18,7 +18,7 @@ import {
} from "office-ui-fabric-react";
import * as React from "react";
import { IGalleryItem } from "../../../../Juno/JunoClient";
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
export interface GalleryCardComponentProps {

View File

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

View File

@@ -1,15 +1,20 @@
import { IButtonProps, IconButton } from "office-ui-fabric-react/lib/Button";
import { ContextualMenu, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
import * as _ from "underscore";
import * as React from "react";
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import {
DetailsList,
DetailsListLayoutMode,
DetailsRow,
IColumn,
IDetailsListProps,
IDetailsRowProps,
DetailsRow,
} from "office-ui-fabric-react/lib/DetailsList";
import { FocusZone } from "office-ui-fabric-react/lib/FocusZone";
import { ITextField, ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { IconButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { IColumn } from "office-ui-fabric-react/lib/DetailsList";
import { IContextualMenuProps, ContextualMenu } from "office-ui-fabric-react/lib/ContextualMenu";
import {
IObjectWithKey,
ISelectionZoneProps,
@@ -17,18 +22,13 @@ import {
SelectionMode,
SelectionZone,
} from "office-ui-fabric-react/lib/utilities/selection/index";
import * as React from "react";
import * as _ from "underscore";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import * as Constants from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { QueriesClient } from "../../../Common/QueriesClient";
import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
const title: string = "Open Saved Queries";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import { QueriesClient } from "../../../Common/QueriesClient";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
export interface QueriesGridComponentProps {
queriesClient: QueriesClient;
@@ -76,11 +76,6 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
}
}
// fetched saved queries when panel open
public componentDidMount() {
this.fetchSavedQueries();
}
public render(): JSX.Element {
if (this.state.queries.length === 0) {
return this.renderBannerComponent();
@@ -141,7 +136,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
},
};
return (
<div id="emptyQueryBanner">
<div>
<div>
You have not saved any queries yet. <br /> <br />
To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save
@@ -227,7 +222,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
const container = window.dataExplorer;
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
paneTitle: container && container.browseQueriesPane.title(),
});
try {
await this.props.queriesClient.deleteQuery(query);
@@ -235,7 +230,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
paneTitle: container && container.browseQueriesPane.title(),
},
startKey
);
@@ -244,7 +239,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
paneTitle: container && container.browseQueriesPane.title(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},

View File

@@ -0,0 +1,33 @@
/**
* This adapter is responsible to render the QueriesGrid React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { QueriesGridComponent, QueriesGridComponentProps } from "./QueriesGridComponent";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import Explorer from "../../Explorer";
export class QueriesGridComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private container: Explorer) {
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
const props: QueriesGridComponentProps = {
queriesClient: this.container.queriesClient,
onQuerySelect: this.container.browseQueriesPane.loadSavedQuery,
containerVisible: this.container.browseQueriesPane.visible(),
saveQueryEnabled: this.container.canSaveQueries(),
};
return <QueriesGridComponent {...props} />;
}
public forceRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -1,18 +1,17 @@
import { shallow } from "enzyme";
import ko from "knockout";
import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as DataModels from "../../../Contracts/DataModels";
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
import { isDirty, TtlType } from "./SettingsUtils";
import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout";
import { TtlType, isDirty } from "./SettingsUtils";
import Explorer from "../../Explorer";
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined),
}));
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
updateCollection: jest.fn().mockReturnValue({
id: undefined,
@@ -22,9 +21,16 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
changeFeedPolicy: undefined,
analyticalStorageTtl: undefined,
geospatialConfig: undefined,
} as DataModels.Collection),
updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({
id: undefined,
shardKey: undefined,
indexes: [],
}),
analyticalStorageTtl: undefined,
} as MongoDBCollectionResource),
}));
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
}));
@@ -128,6 +134,7 @@ describe("SettingsComponent", () => {
loadCollections: undefined,
findCollectionWithId: undefined,
openAddCollection: undefined,
onDeleteDatabaseContextMenuClick: undefined,
readSettings: undefined,
onSettingsClick: undefined,
loadOffer: undefined,
@@ -187,6 +194,7 @@ describe("SettingsComponent", () => {
};
await settingsComponentInstance.onSaveClick();
expect(updateCollection).toBeCalled();
expect(updateMongoDBCollectionThroughRP).toBeCalled();
expect(updateOffer).toBeCalled();
});

View File

@@ -6,7 +6,7 @@ import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
@@ -139,7 +139,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy
);
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
@@ -782,12 +784,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
try {
const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection = {
const newMongoCollection: MongoDBCollectionResource = {
...this.mongoDBCollectionResource,
indexes: newMongoIndexes,
};
this.mongoDBCollectionResource = await updateCollection(
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.collection.databaseId,
this.collection.id(),
newMongoCollection

View File

@@ -12,6 +12,7 @@ import {
TextField,
} from "office-ui-fabric-react";
import React from "react";
import { Features } from "../../../../../Common/Constants";
import * as DataModels from "../../../../../Contracts/DataModels";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import * as SharedConstants from "../../../../../Shared/Constants";
@@ -464,7 +465,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
const oneTBinKB = 1000000000;
const minRUperGB = 10;
const featureFlagEnabled = userContext.features.showMinRUSurvey;
const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey);
const collectionIsEligible =
userContext.subscriptionType !== SubscriptionType.Internal &&
this.props.usageSizeInKB > oneTBinKB &&

View File

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

View File

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

View File

@@ -331,10 +331,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={this.props.getTranslation(placeholderTKey)}
disabled={disabled}
// Removed dropdownWidth="auto" as dropdown accept only number
dropdownWidth="auto"
options={choices.map((c) => ({
key: c.key,
text: this.props.getTranslation(c.labelTKey),
text: this.props.getTranslation(c.label),
}))}
styles={{
root: { width: 400 },

View File

@@ -285,6 +285,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
<StyledWithResponsiveMode
aria-labelledby="database-label"
disabled={true}
dropdownWidth="auto"
id="database-dropdown-input"
onChange={[Function]}
options={
@@ -606,6 +607,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
</StyledLabelBase>
<StyledWithResponsiveMode
aria-labelledby="database-label"
dropdownWidth="auto"
id="database-dropdown-input"
onChange={[Function]}
options={

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,12 +19,13 @@ import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler";
import { trackEvent } from "../Shared/appInsights";
import { appInsights } from "../Shared/appInsights";
import * as SharedConstants from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
@@ -43,32 +44,33 @@ import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPanel } from "./Panes/LoadQueryPanel";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane";
import { SaveQueryPanel } from "./Panes/SaveQueryPanel";
import { SaveQueryPane } from "./Panes/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase";
@@ -94,10 +96,13 @@ export interface ExplorerParams {
closeSidePanel: () => void;
closeDialog: () => void;
openDialog: (props: DialogProps) => void;
tabsManager: TabsManager;
}
export default class Explorer {
public flight: ko.Observable<string> = ko.observable<string>(
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
);
public addCollectionText: ko.Observable<string>;
public addDatabaseText: ko.Observable<string>;
public collectionTitle: ko.Observable<string>;
@@ -105,6 +110,7 @@ export default class Explorer {
public deleteDatabaseText: ko.Observable<string>;
public collectionTreeNodeAltText: ko.Observable<string>;
public refreshTreeTitle: ko.Observable<string>;
public hasWriteAccess: ko.Observable<boolean>;
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
/**
@@ -113,6 +119,11 @@ export default class Explorer {
* */
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
/**
* @deprecated
* Use userContext.subscriptionType instead
* */
public subscriptionType: ko.Observable<SubscriptionType>;
/**
* @deprecated
* Use userContext.apiType instead
@@ -169,6 +180,7 @@ export default class Explorer {
// Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>;
public nonSystemDatabases: ko.Computed<ViewModels.Database[]>;
public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>;
@@ -193,11 +205,22 @@ export default class Explorer {
public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane;
public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane;
public deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane;
public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane;
public tableColumnOptionsPane: TableColumnOptionsPane;
public querySelectPane: QuerySelectPane;
public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane;
public settingsPane: SettingsPane;
public executeSprocParamsPane: ExecuteSprocParamsPane;
public uploadItemsPane: UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: LoadQueryPane;
public saveQueryPane: ContextualPaneBase;
public browseQueriesPane: BrowseQueriesPane;
public uploadFilePane: UploadFilePane;
public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ContextualPaneBase;
@@ -234,6 +257,7 @@ export default class Explorer {
public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = [];
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = (database) => false;
private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager;
@@ -261,6 +285,7 @@ export default class Explorer {
});
this.addCollectionText = ko.observable<string>("New Collection");
this.addDatabaseText = ko.observable<string>("New Database");
this.hasWriteAccess = ko.observable<boolean>(true);
this.collectionTitle = ko.observable<string>("Collections");
this.collectionTreeNodeAltText = ko.observable<string>("Collection");
this.deleteCollectionText = ko.observable<string>("Delete Collection");
@@ -268,6 +293,7 @@ export default class Explorer {
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.isAccountReady = ko.observable<boolean>(false);
this._isInitializingNotebooks = false;
this.arcadiaToken = ko.observable<string>();
@@ -303,7 +329,7 @@ export default class Explorer {
this.isNotebookEnabled(
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) ||
userContext.features.enableNotebooks)
this.isFeatureEnabled(Constants.Features.enableNotebooks))
);
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
@@ -325,10 +351,10 @@ export default class Explorer {
this.isSparkEnabledForAccount() &&
this.arcadiaWorkspaces() &&
this.arcadiaWorkspaces().length > 0) ||
userContext.features.enableSpark
this.isFeatureEnabled(Constants.Features.enableSpark)
);
if (this.isSparkEnabled()) {
trackEvent(
appInsights.trackEvent(
{ name: "LoadedWithSparkEnabled" },
{
subscriptionId: userContext.subscriptionId,
@@ -349,6 +375,7 @@ export default class Explorer {
});
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.features = ko.observable();
this.queriesClient = new QueriesClient(this);
this.resourceTokenDatabaseId = ko.observable<string>();
@@ -360,9 +387,11 @@ export default class Explorer {
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
this.canExceedMaximumValue = ko.computed<boolean>(() => userContext.features.canExceedMaximumValue);
this.canExceedMaximumValue = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
);
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
@@ -442,7 +471,7 @@ export default class Explorer {
});
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) {
if (this.isFeatureEnabled(Constants.Features.enableFixedCollectionWithSharedThroughput)) {
return true;
}
@@ -501,7 +530,20 @@ export default class Explorer {
() =>
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
);
this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2);
this.isRightPanelV2Enabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
);
this.defaultExperience.subscribe((defaultExperience: string) => {
if (
defaultExperience &&
defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase()
) {
this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system";
};
}
});
this.selectedDatabaseId = ko.computed<string>(() => {
const selectedNode = this.selectedNode();
if (!selectedNode) {
@@ -523,6 +565,10 @@ export default class Explorer {
}
});
this.nonSystemDatabases = ko.computed(() => {
return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database));
});
this.addDatabasePane = new AddDatabasePane({
id: "adddatabasepane",
visible: ko.observable<boolean>(false),
@@ -545,6 +591,13 @@ export default class Explorer {
container: this,
});
this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({
id: "deletedatabaseconfirmationpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane",
visible: ko.observable<boolean>(false),
@@ -566,6 +619,20 @@ export default class Explorer {
container: this,
});
this.tableColumnOptionsPane = new TableColumnOptionsPane({
id: "tablecolumnoptionspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.querySelectPane = new QuerySelectPane({
id: "queryselectpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.newVertexPane = new NewVertexPane({
id: "newvertexpane",
visible: ko.observable<boolean>(false),
@@ -580,6 +647,57 @@ export default class Explorer {
container: this,
});
this.settingsPane = new SettingsPane({
id: "settingspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.executeSprocParamsPane = new ExecuteSprocParamsPane({
id: "executesprocparamspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadItemsPane = new UploadItemsPane({
id: "uploaditemspane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane",
visible: ko.observable<boolean>(false),
container: this,
});
this.saveQueryPane = new SaveQueryPane({
id: "savequerypane",
visible: ko.observable<boolean>(false),
container: this,
});
this.browseQueriesPane = new BrowseQueriesPane({
id: "browsequeriespane",
visible: ko.observable<boolean>(false),
container: this,
});
this.uploadFilePane = new UploadFilePane({
id: "uploadfilepane",
visible: ko.observable<boolean>(false),
container: this,
});
this.stringInputPane = new StringInputPane({
id: "stringinputpane",
visible: ko.observable<boolean>(false),
@@ -594,17 +712,27 @@ export default class Explorer {
container: this,
});
this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager = new TabsManager();
this._panes = [
this.addDatabasePane,
this.addCollectionPane,
this.deleteCollectionConfirmationPane,
this.deleteDatabaseConfirmationPane,
this.graphStylingPane,
this.addTableEntityPane,
this.editTableEntityPane,
this.tableColumnOptionsPane,
this.querySelectPane,
this.newVertexPane,
this.cassandraAddCollectionPane,
this.settingsPane,
this.executeSprocParamsPane,
this.uploadItemsPane,
this.loadQueryPane,
this.saveQueryPane,
this.browseQueriesPane,
this.uploadFilePane,
this.stringInputPane,
this.setupNotebooksPane,
];
@@ -700,6 +828,8 @@ export default class Explorer {
this.editTableEntityPane.title("Edit Table Row");
this.deleteCollectionConfirmationPane.title("Delete Table");
this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id");
this.deleteDatabaseConfirmationPane.title("Delete Keyspace");
this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id");
this.tableDataClient = new CassandraAPIDataClient();
break;
}
@@ -753,29 +883,42 @@ export default class Explorer {
});
// Override notebook server parameters from URL parameters
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
this.notebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken,
});
}
const featureSubcription = this.features.subscribe((features) => {
const serverInfo = this.notebookServerInfo();
if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) {
serverInfo.notebookServerEndpoint = features[Constants.Features.notebookServerUrl];
}
if (userContext.features.notebookBasePath) {
this.notebookBasePath(userContext.features.notebookBasePath);
}
if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) {
serverInfo.authToken = features[Constants.Features.notebookServerToken];
}
this.notebookServerInfo(serverInfo);
this.notebookServerInfo.valueHasMutated();
if (userContext.features.livyEndpoint) {
this.sparkClusterConnectionInfo({
userName: undefined,
password: undefined,
endpoints: [
{
endpoint: userContext.features.livyEndpoint,
kind: DataModels.SparkClusterEndpointKind.Livy,
},
],
});
}
if (this.isFeatureEnabled(Constants.Features.notebookBasePath)) {
this.notebookBasePath(features[Constants.Features.notebookBasePath]);
}
if (this.isFeatureEnabled(Constants.Features.livyEndpoint)) {
this.sparkClusterConnectionInfo({
userName: undefined,
password: undefined,
endpoints: [
{
endpoint: features[Constants.Features.livyEndpoint],
kind: DataModels.SparkClusterEndpointKind.Livy,
},
],
});
this.sparkClusterConnectionInfo.valueHasMutated();
}
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
updateUserContext({ useSDKOperations: true });
}
featureSubcription.dispose();
});
}
public openEnableSynapseLinkDialog(): void {
@@ -859,6 +1002,20 @@ export default class Explorer {
return this.selectedNode() == null;
}
public isFeatureEnabled(feature: string): boolean {
const features = this.features();
if (!features) {
return false;
}
if (feature in features && features[feature]) {
return true;
}
return false;
}
public logConsoleData(consoleData: ConsoleData): void {
this.setNotificationConsoleData(consoleData);
}
@@ -917,8 +1074,10 @@ export default class Explorer {
// TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer();
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
@@ -931,16 +1090,20 @@ export default class Explorer {
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
() => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve();
},
(reason) => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
);
},
(error) => {
this._setLoadingStatusText("Failed to fetch databases.");
deferred.reject(error);
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
@@ -1095,12 +1258,12 @@ export default class Explorer {
throw error;
} finally {
// Overwrite with feature flags
if (userContext.features.notebookServerUrl) {
connectionInfo.notebookServerEndpoint = userContext.features.notebookServerUrl;
if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) {
connectionInfo.notebookServerEndpoint = this.features()[Constants.Features.notebookServerUrl];
}
if (userContext.features.notebookServerToken) {
connectionInfo.authToken = userContext.features.notebookServerToken;
if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) {
connectionInfo.authToken = this.features()[Constants.Features.notebookServerToken];
}
this.notebookServerInfo(connectionInfo);
@@ -1216,12 +1379,7 @@ export default class Explorer {
}
public isLastNonEmptyDatabase(): boolean {
if (
this.isLastDatabase() &&
this.databases()[0] &&
this.databases()[0].collections &&
this.databases()[0].collections().length > 0
) {
if (this.isLastDatabase() && this.databases()[0].collections && this.databases()[0].collections().length > 0) {
return true;
}
return false;
@@ -1255,7 +1413,13 @@ export default class Explorer {
if (inputs.defaultCollectionThroughput) {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
}
this.features(inputs.features);
this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType ?? SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.hasWriteAccess(inputs.hasWriteAccess ?? true);
if (inputs.addCollectionDefaultFlight) {
this.flight(inputs.addCollectionDefaultFlight);
}
this.setFeatureFlagsFromFlights(inputs.flights);
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
@@ -2009,6 +2173,38 @@ export default class Explorer {
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId));
}
public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePane.openWithOptions({
paneTitle: "Upload file to notebook server",
selectFileInputLabel: "Select file to upload",
errorMessage: "Could not upload file",
inProgressMessage: "Uploading file to notebook server",
successMessage: "Successfully uploaded file to notebook server",
onSubmit: async (file: File): Promise<NotebookContentItem> => {
const readFileAsText = (inputFile: File): Promise<string> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => {
reader.abort();
reject(`Problem parsing file: ${inputFile}`);
};
reader.onload = () => {
resolve(reader.result as string);
};
reader.readAsText(inputFile);
});
};
const fileContent = await readFileAsText(file);
return this.uploadFile(file.name, fileContent, parent);
},
extensions: undefined,
submitButtonLabel: "Upload",
});
}
public refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled";
@@ -2171,7 +2367,7 @@ export default class Explorer {
public onNewCollectionClicked(): void {
if (this.isPreferredApiCassandra()) {
this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableReactPane) {
} else if (this.isFeatureEnabled(Constants.Features.enableReactPane)) {
this.openAddCollectionPanel();
} else {
this.addCollectionPane.open(this.selectedDatabaseId());
@@ -2212,6 +2408,32 @@ export default class Explorer {
}
}
private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") {
if (!text) {
return;
}
const loadingText = document.getElementById("explorerLoadingStatusText");
if (!loadingText) {
Logger.logError(
"getElementById('explorerLoadingStatusText') failed to find element",
"Explorer/_setLoadingStatusText"
);
return;
}
loadingText.innerHTML = text;
const loadingTitle = document.getElementById("explorerLoadingStatusTitle");
if (!loadingTitle) {
Logger.logError(
"getElementById('explorerLoadingStatusTitle') failed to find element",
"Explorer/_setLoadingStatusText"
);
} else {
loadingTitle.innerHTML = title;
}
}
private _openSetupNotebooksPaneForQuickstart(): void {
const title = "Enable Notebooks (Preview)";
const description =
@@ -2279,7 +2501,7 @@ export default class Explorer {
}
public openDeleteCollectionConfirmationPane(): void {
userContext.features.enableKOPanel
this.isFeatureEnabled(Constants.Features.enableKOPanel)
? this.deleteCollectionConfirmationPane.open()
: this.openSidePanel(
"Delete Collection",
@@ -2291,33 +2513,6 @@ export default class Explorer {
);
}
public openDeleteDatabaseConfirmationPane(): void {
this.openSidePanel(
"Delete Database",
<DeleteDatabaseConfirmationPanel
explorer={this}
openNotificationConsole={this.expandConsole}
closePanel={this.closeSidePanel}
selectedDatabase={this.findSelectedDatabase()}
/>
);
}
public openUploadItemsPanePane(): void {
this.openSidePanel("Upload", <UploadItemsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openSettingPane(): void {
this.openSidePanel("Settings", <SettingsPane explorer={this} closePanel={this.closeSidePanel} />);
}
public openExecuteSprocParamsPanel(): void {
this.openSidePanel(
"Input parameters",
<ExecuteSprocParamsPanel explorer={this} closePanel={() => this.closeSidePanel()} />
);
}
public async openAddCollectionPanel(): Promise<void> {
await this.loadDatabaseOffers();
this.openSidePanel(
@@ -2329,35 +2524,4 @@ export default class Explorer {
/>
);
}
public openBrowseQueriesPanel(): void {
this.openSidePanel("Open Saved Queries", <BrowseQueriesPanel explorer={this} closePanel={this.closeSidePanel} />);
}
public openLoadQueryPanel(): void {
this.openSidePanel("Load Query", <LoadQueryPanel explorer={this} closePanel={() => this.closeSidePanel()} />);
}
public openSaveQueryPanel(): void {
this.openSidePanel("Save Query", <SaveQueryPanel explorer={this} closePanel={() => this.closeSidePanel()} />);
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.openSidePanel(
"Upload File",
<UploadFilePane
explorer={this}
closePanel={this.closeSidePanel}
uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)}
/>
);
}
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
this.openSidePanel(
"Select Column",
<TableQuerySelectPanel explorer={this} closePanel={this.closeSidePanel} queryViewModel={queryViewModal} />
);
}
}

View File

@@ -1,6 +1,6 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import * as GraphData from "./GraphData";
import { NeighborVertexBasicInfo } from "./GraphExplorer";
import * as GraphData from "./GraphData";
import * as ViewModels from "../../../Contracts/ViewModels";
interface JoinArrayMaxCharOutput {
result: string; // string output
@@ -13,9 +13,9 @@ interface EdgePropertyType {
inV?: string;
}
export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => {
export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`;
};
}
/**
* Collect all edges from this node
@@ -23,11 +23,11 @@ export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => {
* @param graphData
* @param newNodes (optional) object describing new nodes encountered
*/
export const createEdgesfromNode = (
export function createEdgesfromNode(
vertex: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
newNodes?: { [id: string]: boolean }
): void => {
): void {
if (Object.prototype.hasOwnProperty.call(vertex, "outE")) {
const outE = vertex.outE;
for (const label in outE) {
@@ -66,7 +66,7 @@ export const createEdgesfromNode = (
});
}
}
};
}
/**
* From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'".
@@ -75,7 +75,7 @@ export const createEdgesfromNode = (
* @param maxSize
* @return
*/
export const getLimitedArrayString = (array: string[], maxSize: number): JoinArrayMaxCharOutput => {
export function getLimitedArrayString(array: string[], maxSize: number): JoinArrayMaxCharOutput {
if (!array || array.length === 0 || array[0].length + 2 > maxSize) {
return { result: "", consumedCount: 0 };
}
@@ -96,16 +96,16 @@ export const getLimitedArrayString = (array: string[], maxSize: number): JoinArr
result: output,
consumedCount: i + 1,
};
};
}
export const createFetchEdgePairQuery = (
export function createFetchEdgePairQuery(
outE: boolean,
pkid: string,
excludedEdgeIds: string[],
startIndex: number,
pageSize: number,
withoutStepArgMaxLenght: number
): string => {
): string {
let gremlinQuery: string;
if (excludedEdgeIds.length > 0) {
// build a string up to max char
@@ -128,15 +128,15 @@ export const createFetchEdgePairQuery = (
}().as('v').select('e', 'v')`;
}
return gremlinQuery;
};
}
/**
* Trim graph
*/
export const trimGraph = (
export function trimGraph(
currentRoot: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
): void => {
) {
const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId);
graphData.unloadAllVertices(importantNodes);
@@ -144,32 +144,32 @@ export const trimGraph = (
$.each(graphData.ids, (index: number, id: string) => {
graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1;
});
};
}
export const addRootChildToGraph = (
export function addRootChildToGraph(
root: GraphData.GremlinVertex,
child: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
): void => {
) {
child._ancestorsId = (root._ancestorsId || []).concat([root.id]);
graphData.addVertex(child);
createEdgesfromNode(child, graphData);
graphData.addNeighborInfo(child);
};
}
/**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now.
* @param value
*/
export const escapeDoubleQuotes = (value: string): string => {
export function escapeDoubleQuotes(value: string): string {
return value === undefined ? value : value.replace(/"/g, '\\"');
};
}
/**
* Surround with double-quotes if val is a string.
* @param val
*/
export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string => {
export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
switch (ip.type) {
case "number":
case "boolean":
@@ -179,12 +179,12 @@ export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string =>
default:
return `"${escapeDoubleQuotes(ip.value as string)}"`;
}
};
}
/**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now.
* @param value
*/
export const escapeSingleQuotes = (value: string): string => {
export function escapeSingleQuotes(value: string): string {
return value === undefined ? value : value.replace(/'/g, "\\'");
};
}

View File

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

View File

@@ -164,7 +164,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon,
iconAlt: label,
onCommandClick: () => container.openSettingPane(),
onCommandClick: () => container.settingsPane.open(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
@@ -407,7 +407,7 @@ function createuploadNotebookButton(container: Explorer): CommandButtonComponent
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.openUploadFilePanel(),
onCommandClick: () => container.onUploadToNotebookServerClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
@@ -420,7 +420,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
onCommandClick: () => container.openBrowseQueriesPanel(),
onCommandClick: () => container.browseQueriesPane.open(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
@@ -433,7 +433,7 @@ function createOpenQueryFromDiskButton(container: Explorer): CommandButtonCompon
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => container.openLoadQueryPanel(),
onCommandClick: () => container.loadQueryPane.open(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,

View File

@@ -1,9 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import { shallow } from "enzyme";
import {
ConsoleDataType,
NotificationConsoleComponent,
NotificationConsoleComponentProps,
NotificationConsoleComponent,
ConsoleDataType,
} from "./NotificationConsoleComponent";
describe("NotificationConsoleComponent", () => {
@@ -12,7 +12,7 @@ describe("NotificationConsoleComponent", () => {
consoleData: undefined,
isConsoleExpanded: false,
inProgressConsoleDataIdToBeDeleted: "",
setIsConsoleExpanded: (): void => undefined,
setIsConsoleExpanded: (isExpanded: boolean): void => {},
};
};
@@ -98,7 +98,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props);
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message);
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`)).toBe(true);
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
};
it("renders progress notifications", () => {
@@ -139,7 +139,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props);
wrapper.find(".clearNotificationsButton").simulate("click");
expect(wrapper.exists(".notificationConsoleData")).toBe(true);
expect(!wrapper.exists(".notificationConsoleData"));
});
it("collapses and hide content", () => {
@@ -155,7 +155,7 @@ describe("NotificationConsoleComponent", () => {
wrapper.setProps(props);
wrapper.find(".notificationConsoleHeader").simulate("click");
expect(wrapper.exists(".notificationConsoleContent")).toBe(false);
expect(!wrapper.exists(".notificationConsoleContent"));
});
it("display latest data in header", () => {

View File

@@ -2,20 +2,19 @@
* React component for control bar
*/
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import * as React from "react";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear.svg";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import LoadingIcon from "../../../../images/loading.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
import InfoIcon from "../../../../images/info_color.svg";
import LoadingIcon from "../../../../images/loading.svg";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import ErrorRedIcon from "../../../../images/error_red.svg";
import ClearIcon from "../../../../images/Clear.svg";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
/**
* Log levels
@@ -77,7 +76,7 @@ export class NotificationConsoleComponent extends React.Component<
public componentDidUpdate(
prevProps: NotificationConsoleComponentProps,
prevState: NotificationConsoleComponentState
): void {
) {
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData);
if (
@@ -98,7 +97,7 @@ export class NotificationConsoleComponent extends React.Component<
}
}
public setElememntRef = (element: HTMLElement): void => {
public setElememntRef = (element: HTMLElement) => {
this.consoleHeaderElement = element;
};
@@ -117,7 +116,7 @@ export class NotificationConsoleComponent extends React.Component<
className="notificationConsoleHeader"
id="notificationConsoleHeader"
ref={this.setElememntRef}
onClick={() => this.expandCollapseConsole()}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0}
>
@@ -136,7 +135,6 @@ export class NotificationConsoleComponent extends React.Component<
<span className="numInfoItems">{numInfoItems}</span>
</span>
</span>
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
<span className="consoleSplitter" />
<span className="headerStatus">
<span className="headerStatusEllipsis">{this.state.headerStatus}</span>
@@ -306,18 +304,3 @@ export class NotificationConsoleComponent extends React.Component<
);
};
}
const PrPreview = (props: { pr: string }) => {
const url = new URL(props.pr);
const [, ref] = url.hash.split("#");
url.hash = "";
return (
<>
<span className="consoleSplitter" />
<a target="_blank" rel="noreferrer" href={url.href} style={{ marginRight: "1em", fontWeight: "bold" }}>
{ref}
</a>
</>
);
};

View File

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

View File

@@ -1,16 +1,15 @@
// Manages all the redux logic for the notebook nteract code
// TODO: Merge with NotebookClient?
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import * as Constants from "../../Common/Constants";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
// Vendor modules
import {
actions,
AppState,
ContentRecord,
createHostRef,
createKernelspecsRef,
HostRecord,
HostRef,
IContentProvider,
KernelspecsRef,
makeAppRecord,
makeCommsRecord,
makeContentsRecord,
@@ -20,21 +19,23 @@ import {
makeJupyterHostRecord,
makeStateRecord,
makeTransformsRecord,
ContentRecord,
HostRecord,
HostRef,
KernelspecsRef,
IContentProvider,
} from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs";
import TransformVDOM from "@nteract/transform-vdom";
import * as Immutable from "immutable";
import { Notification } from "react-notification-system";
import { AnyAction, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import * as Constants from "../../Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux";
import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
import JavaScript from "./NotebookRenderer/outputs/javascript";
import { Notification } from "react-notification-system";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
export type KernelSpecsDisplay = { name: string; displayName: string };
@@ -167,7 +168,7 @@ export class NotebookClientV2 {
"application/vnd.vega.v5+json": NullTransform,
"application/vdom.v1+json": TransformVDOM,
"application/json": Media.Json,
"application/javascript": userContext.features.sandboxNotebookOutputs ? JavaScript : Media.JavaScript,
"application/javascript": Media.JavaScript,
"text/html": Media.HTML,
"text/markdown": Media.Markdown,
"text/latex": Media.LaTeX,

View File

@@ -44,7 +44,7 @@ import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil";
import * as FileSystemUtil from "../FileSystemUtil";
import { FileSystemUtil } from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions";
import { Areas } from "../../../Common/Constants";

View File

@@ -1,12 +1,13 @@
import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import * as DataModels from "../../Contracts/DataModels";
import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import * as StringUtils from "../../Utils/StringUtils";
import { FileSystemUtil } from "./FileSystemUtil";
import { NotebookUtil } from "./NotebookUtil";
import { ServerConfig, IContent, IContentProvider, FileType, IEmptyContent } from "@nteract/core";
import { AjaxResponse } from "rxjs/ajax";
import { stringifyNotebook } from "@nteract/commutable";
export class NotebookContentClient {
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,

View File

@@ -1,20 +1,18 @@
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";
import { userContext } from "../../../UserContext";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core";
import loadTransform from "../NotebookComponent/loadTransform";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import "./NotebookReadOnlyRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs";
export interface NotebookRendererProps {
contentRef: any;
@@ -62,16 +60,6 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<CodeCell id={id} contentRef={contentRef}>
{{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => (
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined,
editor: {
monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,

View File

@@ -1,32 +1,37 @@
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, MarkdownCell, RawCell } from "@nteract/stateful-components";
import * as React from "react";
import "./base.css";
import "./default.css";
import { RawCell, Cells, CodeCell, MarkdownCell } 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";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
import CellLabeler from "./decorators/CellLabeler";
import HoverableCell from "./decorators/HoverableCell";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import "./default.css";
import "./NotebookRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs";
import Prompt from "./Prompt";
import { promptContent } from "./PromptContent";
import StatusBar from "./StatusBar";
import { AzureTheme } from "./AzureTheme";
import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core";
import { CellId } from "@nteract/commutable";
import loadTransform from "../NotebookComponent/loadTransform";
import DraggableCell from "./decorators/draggable";
import CellCreator from "./decorators/CellCreator";
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
import CellToolbar from "./Toolbar";
import StatusBar from "./StatusBar";
import HijackScroll from "./decorators/hijack-scroll";
import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler";
import * as cdbActions from "../NotebookComponent/actions";
export interface NotebookRendererBaseProps {
contentRef: any;
@@ -107,16 +112,6 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
</Prompt>
),
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
outputs: userContext.features.sandboxNotebookOutputs
? (props: any) => (
<IFrameOutputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</IFrameOutputs>
)
: undefined,
}}
</CodeCell>
),

View File

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

View File

@@ -1,64 +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;
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() {
this.resizeObserver?.disconnect();
}
onFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
const doc = (event.target as HTMLIFrameElement).contentDocument;
copyStyles(document, doc);
this.setState({
frameBody: doc.body,
frameHeight: doc.body.scrollHeight,
});
this.resizeObserver = new ResizeObserver(() =>
this.setState({
frameHeight: this.state.frameBody.scrollHeight,
})
);
this.resizeObserver.observe(doc.body);
}
}

View File

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

View File

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

View File

@@ -105,6 +105,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseId = ko.observable<string>();
this.databaseCreateNew = ko.observable<boolean>(true);
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.container.subscriptionType &&
this.container.subscriptionType.subscribe((subscriptionType) => {
this.databaseCreateNewShared(this.getSharedThroughputDefault());
});
this.collectionWithThroughputInShared = ko.observable<boolean>(false);
this.databaseIds = ko.observableArray<string>();
this.uniqueKeys = ko.observableArray<DynamicListItem>();
@@ -474,6 +478,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
});
this.resetData();
this.container.flight.subscribe(() => {
this.resetData();
});
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
@@ -652,7 +659,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
public getSharedThroughputDefault(): boolean {
const subscriptionType = userContext.subscriptionType;
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
@@ -694,12 +701,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
partitionKey: this.partitionKey(),
databaseId: this.databaseId(),
}),
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: this._getThroughput(),
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
@@ -798,12 +805,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
}),
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput,
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
useIndexingForSharedThroughput: this.useIndexingForSharedThroughput(),
@@ -870,12 +877,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
}),
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput,
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
@@ -902,12 +909,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
},
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput,
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,
@@ -987,7 +994,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.container.openEnableSynapseLinkDialog();
}
public ttl90DaysEnabled: () => boolean = () => userContext.features.ttl90Days;
public ttl90DaysEnabled: () => boolean = () => this.container.isFeatureEnabled(Constants.Features.ttl90Days);
public isValid(): boolean {
// TODO add feature flag that disables validation for customers with custom accounts
@@ -1195,7 +1202,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
if (this.isAnalyticalStorageOn()) {
// TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days
return this.container.isFeatureEnabled(Constants.Features.ttl90Days)
? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite;
}

View File

@@ -905,7 +905,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
if (this.state.enableAnalyticalStore) {
// TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days
return this.props.explorer.isFeatureEnabled(Constants.Features.ttl90Days)
? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite;
}

View File

@@ -1,9 +1,8 @@
import * as Constants from "../../Common/Constants";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import AddDatabasePane from "./AddDatabasePane";
import { DatabaseAccount } from "../../Contracts/DataModels";
describe("Add Database Pane", () => {
describe("getSharedThroughputDefault()", () => {
@@ -45,41 +44,31 @@ describe("Add Database Pane", () => {
});
it("should be true if subscription type is Benefits", () => {
updateUserContext({
subscriptionType: SubscriptionType.Benefits,
});
explorer.subscriptionType(SubscriptionType.Benefits);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be false if subscription type is EA", () => {
updateUserContext({
subscriptionType: SubscriptionType.EA,
});
explorer.subscriptionType(SubscriptionType.EA);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
});
it("should be true if subscription type is Free", () => {
updateUserContext({
subscriptionType: SubscriptionType.Free,
});
explorer.subscriptionType(SubscriptionType.Free);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is Internal", () => {
updateUserContext({
subscriptionType: SubscriptionType.Internal,
});
explorer.subscriptionType(SubscriptionType.Internal);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is PAYG", () => {
updateUserContext({
subscriptionType: SubscriptionType.PAYG,
});
explorer.subscriptionType(SubscriptionType.PAYG);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});

View File

@@ -61,6 +61,11 @@ export default class AddDatabasePane extends ContextualPaneBase {
// TODO 388844: get defaults from parent frame
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.container.subscriptionType &&
this.container.subscriptionType.subscribe((subscriptionType) => {
this.databaseCreateNewShared(this.getSharedThroughputDefault());
});
this.databaseIdLabel = ko.computed<string>(() =>
this.container.isPreferredApiCassandra() ? "Keyspace id" : "Database id"
);
@@ -226,6 +231,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
});
this.resetData();
this.container.flight.subscribe(() => {
this.resetData();
});
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
@@ -268,11 +276,11 @@ export default class AddDatabasePane extends ContextualPaneBase {
super.open();
this.resetData();
const addDatabasePaneOpenMessage = {
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
@@ -294,10 +302,10 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared(),
}),
offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
@@ -337,7 +345,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}
public getSharedThroughputDefault(): boolean {
const subscriptionType = userContext.subscriptionType;
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
@@ -356,10 +364,10 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
@@ -378,10 +386,10 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,

View File

@@ -0,0 +1,33 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="browsequeriespane">
<!-- Save Query form -- Start -->
<div class="contextual-pane-in">
<div class="paneContentContainer">
<!-- Save Query header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Save Query header - End -->
<!-- Save Query inputs - Start -->
<div class="paneMainContent"><div class="pkPadding" data-bind="react: queriesGridComponentAdapter"></div></div>
</div>
</div>
<!-- Save Query form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,100 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants";
import { ContextualPaneBase } from "./ContextualPaneBase";
import * as Logger from "../../Common/Logger";
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class BrowseQueriesPane extends ContextualPaneBase {
public queriesGridComponentAdapter: QueriesGridComponentAdapter;
public canSaveQueries: ko.Computed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Open Saved Queries");
this.resetData();
this.canSaveQueries = this.container && this.container.canSaveQueries;
this.queriesGridComponentAdapter = new QueriesGridComponentAdapter(this.container);
}
public open() {
super.open();
this.queriesGridComponentAdapter.forceRender();
}
public close() {
super.close();
this.queriesGridComponentAdapter.forceRender();
}
public submit() {
// override default behavior because this is not a form
}
public setupQueries = async (src: any, event: MouseEvent): Promise<void> => {
if (!this.container) {
return;
}
const startKey: number = TelemetryProcessor.traceStart(Action.SetupSavedQueries, {
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
});
try {
this.isExecuting(true);
await this.container.queriesClient.setupQueriesCollection();
this.container.refreshAllDatabases().done(() => this.queriesGridComponentAdapter.forceRender());
TelemetryProcessor.traceSuccess(
Action.SetupSavedQueries,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
},
startKey
);
} catch (error) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.SetupSavedQueries,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
this.formErrors(`Failed to setup a collection for saved queries: ${errorMessage}`);
} finally {
this.isExecuting(false);
}
};
public loadSavedQuery = (savedQuery: DataModels.Query): void => {
const selectedCollection: ViewModels.Collection = this.container && this.container.findSelectedCollection();
if (!selectedCollection) {
// should never get into this state because this pane is only accessible through the query tab
Logger.logError("No collection was selected", "BrowseQueriesPane.loadSavedQuery");
return;
} else if (this.container.isPreferredApiMongoDB()) {
selectedCollection.onNewMongoQueryClick(selectedCollection, null);
} else {
selectedCollection.onNewQueryClick(selectedCollection, null);
}
const queryTab = this.container.tabsManager.activeTab() as QueryTab;
queryTab.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
queryTab.initialEditorContent(savedQuery.query);
queryTab.sqlQueryEditorContent(savedQuery.query);
TelemetryProcessor.trace(Action.LoadSavedQuery, ActionModifiers.Mark, {
dataExplorerArea: Areas.ContextualPane,
queryName: savedQuery.queryName,
paneTitle: this.title(),
});
this.close();
};
}

View File

@@ -1,58 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Browse queries panel Should render Default properly 1`] = `
<BrowseQueriesPanel
closePanel={[Function]}
explorer={
Object {
"canSaveQueries": [Function],
"queriesClient": Object {
"getQueries": [Function],
},
}
}
>
<div
className="panelFormWrapper"
>
<div
className="panelMainContent"
>
<QueriesGridComponent
containerVisible={true}
onQuerySelect={[Function]}
queriesClient={
Object {
"getQueries": [Function],
}
}
saveQueryEnabled={true}
>
<div
id="emptyQueryBanner"
>
<div>
You have not saved any queries yet.
<br />
<br />
To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save Query and follow the prompt in order to save the query.
</div>
<img
alt="Save query helper banner"
src=""
style={
Object {
"border": "1px solid undefined",
"height": "150px",
"marginTop": "20px",
"width": "310px",
}
}
/>
</div>
</QueriesGridComponent>
</div>
</div>
</BrowseQueriesPanel>
`;

View File

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

View File

@@ -1,63 +0,0 @@
import React, { FunctionComponent } from "react";
import { Areas } from "../../../Common/Constants";
import { logError } from "../../../Common/Logger";
import { Query } from "../../../Contracts/DataModels";
import { Collection } from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import {
QueriesGridComponent,
QueriesGridComponentProps,
} from "../../Controls/QueriesGridReactComponent/QueriesGridComponent";
import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab";
interface BrowseQueriesPanelProps {
explorer: Explorer;
closePanel: () => void;
}
export const BrowseQueriesPanel: FunctionComponent<BrowseQueriesPanelProps> = ({
explorer,
closePanel,
}: BrowseQueriesPanelProps): JSX.Element => {
const loadSavedQuery = (savedQuery: Query): void => {
const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
if (!selectedCollection) {
// should never get into this state because this pane is only accessible through the query tab
logError("No collection was selected", "BrowseQueriesPane.loadSavedQuery");
return;
} else if (userContext.apiType === "Mongo") {
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
} else {
selectedCollection.onNewQueryClick(selectedCollection, undefined);
}
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
queryTab.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
queryTab.initialEditorContent(savedQuery.query);
queryTab.sqlQueryEditorContent(savedQuery.query);
trace(Action.LoadSavedQuery, ActionModifiers.Mark, {
dataExplorerArea: Areas.ContextualPane,
queryName: savedQuery.queryName,
paneTitle: "Open Saved Queries",
});
closePanel();
};
const props: QueriesGridComponentProps = {
queriesClient: explorer.queriesClient,
onQuerySelect: loadSavedQuery,
containerVisible: true,
saveQueryEnabled: explorer.canSaveQueries(),
};
return (
<div className="panelFormWrapper">
<div className="panelMainContent">
<QueriesGridComponent {...props} />
</div>
</div>
);
};

View File

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

View File

@@ -5,6 +5,7 @@ import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as SharedConstants from "../../Shared/Constants";
@@ -116,6 +117,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.resetData();
this.container.flight.subscribe(() => {
this.resetData();
});
this.requestUnitsUsageCostDedicated = ko.computed(() => {
const account = this.container.databaseAccount();
if (!account) {
@@ -256,8 +261,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
});
this.keyspaceIds(cachedKeyspaceIdsList);
};
this.container.databases.subscribe((newDatabases: ViewModels.Database[]) => updateKeyspaceIds(newDatabases));
updateKeyspaceIds(this.container.databases());
this.container.nonSystemDatabases.subscribe((newDatabases: ViewModels.Database[]) =>
updateKeyspaceIds(newDatabases)
);
updateKeyspaceIds(this.container.nonSystemDatabases());
}
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
@@ -301,12 +308,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
partitionKey: "",
databaseId: this.keyspaceId(),
}),
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
@@ -353,12 +360,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput(),
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
@@ -397,12 +404,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput(),
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
@@ -425,12 +432,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput(),
},
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType,
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
flight: this.container.flight(),
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,

View File

@@ -0,0 +1,109 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="deletedatabaseconfirmationpane">
<!-- Delete Databaes Confirmation form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Delete Database Confirmation header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Delete Database Confirmation header - End -->
<div class="warningErrorContainer" data-bind="visible: !formErrors()">
<div class="warningErrorContent">
<span><img class="paneWarningIcon" src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer">
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this
resource and all of its children resources.
</span>
</div>
</div>
<!-- Delete Database Confirmation errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="
visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a class="errorLink" role="link" data-bind="click: showErrorDetails">More details</a>
</span>
</div>
</div>
<!-- Delete Database Confirmation errors - End -->
<!-- Delete Database Confirmation inputs - Start -->
<div class="paneMainContent">
<div>
<span class="mandatoryStar">*</span> <span data-bind="text: databaseIdConfirmationText"></span>
<p>
<input
type="text"
name="databaseIdConfirmation"
data-test="confirmDatabaseId"
required
class="collid"
data-bind="value: databaseIdConfirmation, hasFocus: firstFieldHasFocus"
aria-label="Confirm by typing the database id"
/>
</p>
</div>
<div data-bind="visible: recordDeleteFeedback">
<div>Help us improve Azure Cosmos DB!</div>
<div>What is the reason why you are deleting this database?</div>
<p>
<textarea
type="text"
data-test="databaseDeleteFeedback"
name="databaseDeleteFeedback"
rows="3"
cols="53"
maxlength="512"
class="collid"
data-bind="value: databaseDeleteFeedback"
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
>
</textarea>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="deleteDatabase" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Delete Database Confirmation inputs - End -->
</form>
</div>
<!-- Delete Database Confirmation form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,88 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="loadQueryPane">
<!-- Load Query form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Load Query header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Load Query header - End -->
<!-- Load Query errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Load Query errors - End -->
<!-- Load Query inputs - Start -->
<div class="paneMainContent">
<div>
<div class="renewUploadItemsHeader">Select a query document</div>
<input
class="importFilesTitle"
type="text"
role="textbox"
disabled
data-bind="value: selectedFilesTitle"
aria-label="Select a query document"
autofocus
/>
<input
type="file"
id="importQueryInput"
accept="text/plain"
style="display: none"
data-bind="event: { change: updateSelectedFiles }"
/>
<a
href="#"
id="queryFileImportLink"
aria-label="Upload files"
tabindex="0"
role="button"
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
>
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="upload files" />
</a>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="Load" class="btncreatecoll1" /></div>
</div>
<!-- Load Query inputs - End -->
</form>
</div>
<!-- Load Query form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,147 @@
import * as ko from "knockout";
import * as Q from "q";
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as Logger from "../../Common/Logger";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import QueryTab from "../Tabs/QueryTab";
export class LoadQueryPane extends ContextualPaneBase {
public selectedFilesTitle: ko.Observable<string>;
public files: ko.Observable<FileList>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Load Query");
this.resetData();
this.selectedFilesTitle = ko.observable<string>("");
this.files = ko.observable<FileList>();
this.files.subscribe((newFiles: FileList) => this.updateSelectedFilesTitle(newFiles));
const focusElement = document.getElementById("queryFileImportLink");
focusElement && focusElement.focus();
}
public submit() {
this.formErrors("");
this.formErrorsDetails("");
if (!this.files() || this.files().length === 0) {
this.formErrors("No file specified");
this.formErrorsDetails("No file specified. Please input a file.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not load query -- No file specified. Please input a file."
);
return;
}
const file: File = this.files().item(0);
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Loading query from file ${file.name}`
);
this.isExecuting(true);
this.loadQueryFromFile(this.files().item(0))
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully loaded query from file ${file.name}`
);
this.close();
},
(error: any) => {
this.formErrors("Failed to load query");
this.formErrorsDetails(`Failed to load query: ${error}`);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to load query from file ${file.name}: ${error}`
);
}
)
.finally(() => {
this.isExecuting(false);
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
}
public updateSelectedFiles(element: any, event: any): void {
this.files(event.target.files);
}
public open() {
super.open();
const focusElement = document.getElementById("queryFileImportLink");
focusElement && focusElement.focus();
}
public close() {
super.close();
this.resetData();
this.files(undefined);
this.resetFileInput();
}
public onImportLinkClick(source: any, event: MouseEvent): boolean {
document.getElementById("importQueryInput").click();
return false;
}
public onImportLinkKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
this.onImportLinkClick(source, null);
return false;
}
return true;
};
public loadQueryFromFile(file: File): Q.Promise<void> {
const selectedCollection: ViewModels.Collection = this.container && this.container.findSelectedCollection();
if (!selectedCollection) {
// should never get into this state
Logger.logError("No collection was selected", "LoadQueryPane.loadQueryFromFile");
return Q.reject("No collection was selected");
} else if (this.container.isPreferredApiMongoDB()) {
selectedCollection.onNewMongoQueryClick(selectedCollection, null);
} else {
selectedCollection.onNewQueryClick(selectedCollection, null);
}
const deferred: Q.Deferred<void> = Q.defer<void>();
const reader = new FileReader();
reader.onload = (evt: any): void => {
const fileData: string = evt.target.result;
const queryTab = this.container.tabsManager.activeTab() as QueryTab;
queryTab.initialEditorContent(fileData);
queryTab.sqlQueryEditorContent(fileData);
deferred.resolve();
};
reader.onerror = (evt: ProgressEvent): void => {
deferred.reject((evt as any).error.message);
};
reader.readAsText(file);
return deferred.promise;
}
private updateSelectedFilesTitle(fileList: FileList) {
this.selectedFilesTitle("");
if (!fileList || fileList.length === 0) {
return;
}
for (let i = 0; i < fileList.length; i++) {
const originalTitle = this.selectedFilesTitle();
this.selectedFilesTitle(originalTitle + `"${fileList.item(i).name}"`);
}
}
private resetFileInput(): void {
const inputElement = $("#importQueryInput");
inputElement.wrap("<form>").closest("form").get(0).reset();
inputElement.unwrap();
}
}

View File

@@ -1,62 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Load Query Pane should render Default properly 1`] = `
<GenericRightPaneComponent
container={Object {}}
formError=""
formErrorDetail=""
id="loadQueryPane"
isExecuting={false}
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Load"
title="Load Query"
>
<div
className="panelFormWrapper"
>
<div
className="panelMainContent"
>
<Stack
horizontal={true}
>
<StyledTextFieldBase
autoFocus={true}
id="confirmCollectionId"
label="Select a query document"
readOnly={true}
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
value=""
/>
<label
className="customFileUpload"
htmlFor="importQueryInputId"
>
<StyledImageBase
alt="upload files"
className="fileIcon"
height={20}
imageFit={4}
src=""
width={20}
/>
<input
accept="text/plain"
className="fileUpload"
id="importQueryInputId"
onChange={[Function]}
type="file"
/>
</label>
</Stack>
</div>
</div>
</GenericRightPaneComponent>
`;

View File

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

View File

@@ -1,134 +0,0 @@
import { useBoolean } from "@uifabric/react-hooks";
import { IImageProps, Image, ImageFit, Stack, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import folderIcon from "../../../../images/folder_16x16.svg";
import { logError } from "../../../Common/Logger";
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";
interface LoadQueryPanelProps {
explorer: Explorer;
closePanel: () => void;
}
export const LoadQueryPanel: FunctionComponent<LoadQueryPanelProps> = ({
explorer,
closePanel,
}: LoadQueryPanelProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [selectedFileName, setSelectedFileName] = useState<string>("");
const [selectedFiles, setSelectedFiles] = useState<FileList>();
const imageProps: Partial<IImageProps> = {
imageFit: ImageFit.centerCover,
width: 20,
height: 20,
className: "fileIcon",
};
const title = "Load Query";
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: formError,
formErrorDetail: formErrorsDetails,
id: "loadQueryPane",
isExecuting: isLoading,
title,
submitButtonText: "Load",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const onFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { files } = e.target;
setSelectedFiles(files);
setSelectedFileName(files && files[0] && `"${files[0].name}"`);
};
const submit = async (): Promise<void> => {
setFormError("");
setFormErrorsDetails("");
if (!selectedFiles || selectedFiles.length === 0) {
setFormError("No file specified");
setFormErrorsDetails("No file specified. Please input a file.");
logConsoleError("Could not load query -- No file specified. Please input a file.");
return;
}
const file: File = selectedFiles[0];
logConsoleProgress(`Loading query from file ${file.name}`);
setLoadingTrue();
try {
await loadQueryFromFile(file);
logConsoleInfo(`Successfully loaded query from file ${file.name}`);
closePanel();
setLoadingFalse();
} catch (error) {
setLoadingFalse();
setFormError("Failed to load query");
setFormErrorsDetails(`Failed to load query: ${error}`);
logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
}
};
const loadQueryFromFile = async (file: File): Promise<void> => {
const selectedCollection: Collection = explorer?.findSelectedCollection();
if (!selectedCollection) {
logError("No collection was selected", "LoadQueryPane.loadQueryFromFile");
} else if (userContext.apiType === "Mongo") {
selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
} else {
selectedCollection.onNewQueryClick(selectedCollection, undefined);
}
const reader = new FileReader();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reader.onload = (evt: any): void => {
const fileData: string = evt.target.result;
const queryTab = explorer.tabsManager.activeTab() as QueryTab;
queryTab.initialEditorContent(fileData);
queryTab.sqlQueryEditorContent(fileData);
};
reader.onerror = (): void => {
setFormError("Failed to load query");
setFormErrorsDetails(`Failed to load query`);
logConsoleError(`Failed to load query from file ${file.name}`);
};
return reader.readAsText(file);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<Stack horizontal>
<TextField
id="confirmCollectionId"
label="Select a query document"
value={selectedFileName}
autoFocus
readOnly
styles={{ fieldGroup: { width: 300 } }}
/>
<label htmlFor="importQueryInputId" className="customFileUpload">
<Image {...imageProps} src={folderIcon} alt="upload files" />
<input
className="fileUpload"
type="file"
id="importQueryInputId"
accept="text/plain"
onChange={onFileSelected}
/>
</label>
</Stack>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

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

View File

@@ -110,48 +110,7 @@
.deleteCollectionFeedback {
margin-top: 12px;
}
.addRemoveIcon {
margin-left: 4px !important;
}
.addRemoveIconLabel {
margin-top: 28px;
margin-left: 4px !important;
}
.addNewParamStyle {
margin-top: 5px;
margin-left: 5px !important;
cursor: pointer;
}
.panelGroupSpacing > * {
margin-bottom: @SmallSpace;
}
.fileUpload {
display: none !important;
}
.customFileUpload {
padding: 25px 0px 0px 10px;
cursor: pointer;
display: flex;
}
.fileIcon {
align-self: center;
}
.panelAddIconLabel {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.panelAddIcon {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.removeIcon {
color: @InfoIconColor;
}
.column-select-view {
margin: 20px 0px 0px 0px;
}

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="savequerypane">
<!-- Save Query form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Save Query header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Save Query header - End -->
<!-- Save Query errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- Save Query errors - End -->
<!-- Save Query inputs - Start -->
<div class="paneMainContent">
<div class="pkPadding" data-bind="visible: !canSaveQueries()">
<div data-bind="text: setupSaveQueriesText"></div>
<button class="btncreatecoll1 btnSetupQueries" type="button" data-bind="click: setupQueries">
Complete setup
</button>
</div>
<div class="pkPadding" data-bind="visible: canSaveQueries">
<p><span class="mandatoryStar">*</span> <span>Name</span></p>
<input class="textfontclr collid" required type="text" data-bind="value: queryName" />
</div>
</div>
<div class="paneFooter" data-bind="visible: canSaveQueries">
<div class="leftpanel-okbut"><input type="submit" value="Save" class="btncreatecoll1" /></div>
</div>
<!-- Save Query inputs - End -->
</form>
</div>
<!-- Save Query form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -0,0 +1,153 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class SaveQueryPane extends ContextualPaneBase {
public queryName: ko.Observable<string>;
public canSaveQueries: ko.Computed<boolean>;
public setupSaveQueriesText: string = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${Constants.SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Save Query");
this.queryName = ko.observable<string>();
this.canSaveQueries = this.container && this.container.canSaveQueries;
this.resetData();
}
public submit = (): void => {
this.formErrors("");
this.formErrorsDetails("");
if (!this.canSaveQueries()) {
this.formErrors("Cannot save query");
this.formErrorsDetails("Failed to save query: account not set up to save queries");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Failed to save query: account not setup to save queries"
);
}
const queryName: string = this.queryName();
const queryTab = this.container && (this.container.tabsManager.activeTab() as QueryTab);
const query: string = queryTab && queryTab.sqlQueryEditorContent();
if (!queryName || queryName.length === 0) {
this.formErrors("No query name specified");
this.formErrorsDetails("No query name specified. Please specify a query name.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not save query -- No query name specified. Please specify a query name."
);
return;
} else if (!query || query.length === 0) {
this.formErrors("Invalid query content specified");
this.formErrorsDetails("Invalid query content specified. Please enter query content.");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
"Could not save query -- Invalid query content specified. Please enter query content."
);
return;
}
const queryParam: DataModels.Query = {
id: queryName,
resourceId: this.container.queriesClient.getResourceId(),
queryName: queryName,
query: query,
};
const startKey: number = TelemetryProcessor.traceStart(Action.SaveQuery, {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
});
this.isExecuting(true);
this.container.queriesClient.saveQuery(queryParam).then(
() => {
this.isExecuting(false);
queryTab.tabTitle(queryParam.queryName);
queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
TelemetryProcessor.traceSuccess(
Action.SaveQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
},
startKey
);
this.close();
},
(error: any) => {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors("Failed to save query");
this.formErrorsDetails(`Failed to save query: ${errorMessage}`);
TelemetryProcessor.traceFailure(
Action.SaveQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
);
};
public setupQueries = async (src: any, event: MouseEvent): Promise<void> => {
if (!this.container) {
return;
}
const startKey: number = TelemetryProcessor.traceStart(Action.SetupSavedQueries, {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
});
try {
this.isExecuting(true);
await this.container.queriesClient.setupQueriesCollection();
this.container.refreshAllDatabases();
TelemetryProcessor.traceSuccess(
Action.SetupSavedQueries,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
},
startKey
);
} catch (error) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.SetupSavedQueries,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
this.formErrors("Failed to setup a container for saved queries");
this.formErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally {
this.isExecuting(false);
}
};
public close() {
super.close();
this.resetData();
}
public resetData() {
super.resetData();
this.queryName("");
}
}

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Save Query Pane should render Default properly 1`] = `
<GenericRightPaneComponent
container={
Object {
"canSaveQueries": [Function],
}
}
formError=""
formErrorDetail=""
id="saveQueryPane"
isExecuting={false}
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Complete setup"
title="Save Query"
>
<div
className="panelFormWrapper"
>
<div
className="panelMainContent"
>
<Text
variant="small"
>
For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “___Cosmos”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.
</Text>
</div>
</div>
</GenericRightPaneComponent>
`;

View File

@@ -1,32 +0,0 @@
import { shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import Explorer from "../../Explorer";
import { SaveQueryPanel } from "./index";
describe("Save Query Pane", () => {
const fakeExplorer = {} as Explorer;
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
};
const wrapper = shallow(<SaveQueryPanel {...props} />);
it("should return true if can save Queries else false", () => {
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
wrapper.setProps(props);
expect(wrapper.exists("#saveQueryInput")).toBe(true);
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => false);
wrapper.setProps(props);
expect(wrapper.exists("#saveQueryInput")).toBe(false);
});
it("should render Default properly", () => {
const wrapper = shallow(<SaveQueryPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,168 +0,0 @@
import { useBoolean } from "@uifabric/react-hooks";
import { Text, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Query } from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
interface SaveQueryPanelProps {
explorer: Explorer;
closePanel: () => void;
}
export const SaveQueryPanel: FunctionComponent<SaveQueryPanelProps> = ({
explorer,
closePanel,
}: SaveQueryPanelProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [queryName, setQueryName] = useState<string>("");
const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
const title = "Save Query";
const { canSaveQueries } = explorer;
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: formError,
formErrorDetail: formErrorsDetails,
id: "saveQueryPane",
isExecuting: isLoading,
title,
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
onClose: () => closePanel(),
onSubmit: () => {
canSaveQueries() ? submit() : setupQueries();
},
};
const submit = async (): Promise<void> => {
setFormError("");
setFormErrorsDetails("");
if (!canSaveQueries()) {
setFormError("Cannot save query");
setFormErrorsDetails("Failed to save query: account not set up to save queries");
logConsoleError("Failed to save query: account not setup to save queries");
}
const queryTab = explorer && (explorer.tabsManager.activeTab() as QueryTab);
const query: string = queryTab && queryTab.sqlQueryEditorContent();
if (!queryName || queryName.length === 0) {
setFormError("No query name specified");
setFormErrorsDetails("No query name specified. Please specify a query name.");
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
return;
} else if (!query || query.length === 0) {
setFormError("Invalid query content specified");
setFormErrorsDetails("Invalid query content specified. Please enter query content.");
logConsoleError("Could not save query -- Invalid query content specified. Please enter query content.");
return;
}
const queryParam: Query = {
id: queryName,
resourceId: explorer.queriesClient.getResourceId(),
queryName: queryName,
query: query,
};
const startKey: number = traceStart(Action.SaveQuery, {
dataExplorerArea: Areas.ContextualPane,
paneTitle: title,
});
setLoadingTrue();
try {
await explorer.queriesClient.saveQuery(queryParam);
setLoadingFalse();
queryTab.tabTitle(queryParam.queryName);
queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
traceSuccess(
Action.SaveQuery,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: title,
},
startKey
);
closePanel();
} catch (error) {
setLoadingFalse();
const errorMessage = getErrorMessage(error);
setFormError("Failed to save query");
setFormErrorsDetails(`Failed to save query: ${errorMessage}`);
traceFailure(
Action.SaveQuery,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: title,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
}
};
const setupQueries = async (): Promise<void> => {
const startKey: number = traceStart(Action.SetupSavedQueries, {
dataExplorerArea: Areas.ContextualPane,
paneTitle: title,
});
try {
setLoadingTrue();
await explorer.queriesClient.setupQueriesCollection();
explorer.refreshAllDatabases();
traceSuccess(
Action.SetupSavedQueries,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: title,
},
startKey
);
} catch (error) {
const errorMessage = getErrorMessage(error);
traceFailure(
Action.SetupSavedQueries,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: title,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
setFormError("Failed to setup a container for saved queries");
setFormErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally {
setLoadingFalse();
}
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
{!canSaveQueries() ? (
<Text variant="small">{setupSaveQueriesText}</Text>
) : (
<TextField
id="saveQueryInput"
label="Name"
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
setQueryName(newInput);
}}
/>
)}
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

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

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