Compare commits

..

1 Commits

Author SHA1 Message Date
Tanuj Mittal
9aeb349d74 Sandbox HTML and JavaScript outputs in iFrame 2021-04-02 17:27:17 -07:00
120 changed files with 33377 additions and 11103 deletions

View File

@@ -126,15 +126,19 @@ src/Explorer/Panes/DeleteCollectionConfirmationPane.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/GraphStylingPane.ts src/Explorer/Panes/GraphStylingPane.ts
src/Explorer/Panes/LoadQueryPane.ts
src/Explorer/Panes/NewVertexPane.ts src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SaveQueryPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/StringInputPane.ts src/Explorer/Panes/StringInputPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/AddTableEntityPane.ts src/Explorer/Panes/Tables/AddTableEntityPane.ts
src/Explorer/Panes/Tables/EditTableEntityPane.ts src/Explorer/Panes/Tables/EditTableEntityPane.ts
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
src/Explorer/Panes/Tables/QuerySelectPane.ts
src/Explorer/Panes/Tables/TableColumnOptionsPane.ts
src/Explorer/Panes/Tables/TableEntityPane.ts src/Explorer/Panes/Tables/TableEntityPane.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
@@ -298,6 +302,8 @@ src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.t
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.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/NotebookComponent.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx

View File

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

@@ -70,6 +70,7 @@ jobs:
- run: npm run test - run: npm run test
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint, format, compile, unittest]
name: "Build" name: "Build"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -91,14 +92,6 @@ jobs:
with: with:
name: dist name: dist
path: 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: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Emulator Tests"
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')

32273
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", "@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.2", "@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3", "@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1", "@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.4.2", "@nteract/commutable": "7.4.2",
"@nteract/connected-components": "6.8.2", "@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0", "@nteract/core": "15.1.0",
@@ -25,12 +25,12 @@
"@nteract/iron-icons": "1.0.0", "@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "2.0.0", "@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0", "@nteract/logos": "1.0.0",
"@nteract/markdown": "4.6.1", "@nteract/markdown": "4.4.0",
"@nteract/monaco-editor": "3.2.2", "@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0", "@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9", "@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7", "@nteract/presentational-components": "3.0.7",
"@nteract/stateful-components": "1.7.7", "@nteract/stateful-components": "1.7.0",
"@nteract/styles": "2.0.2", "@nteract/styles": "2.0.2",
"@nteract/transform-geojson": "5.1.8", "@nteract/transform-geojson": "5.1.8",
"@nteract/transform-model-debug": "5.0.1", "@nteract/transform-model-debug": "5.0.1",
@@ -43,6 +43,7 @@
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110", "@uifabric/react-cards": "0.109.110",
"@uifabric/react-hooks": "7.14.0",
"@uifabric/styling": "7.13.7", "@uifabric/styling": "7.13.7",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
@@ -170,7 +171,7 @@
"ts-loader": "6.2.2", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",
"typescript": "4.2.3", "typescript": "4.0.2",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "4.43.0", "webpack": "4.43.0",
@@ -200,8 +201,8 @@
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts", "build:contracts": "npm run compile:contracts",
"strict:find": "node ./strict-null-checks/find.js", "strictEligibleFiles": "node ./strict-migration-tools/index.js",
"strict:add": "node ./strict-null-checks/auto-add.js", "autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks", "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts" "generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
}, },

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

@@ -32,7 +32,7 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
}; };
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => { 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(); requestContext.headers["x-ms-proxy-target"] = endpoint();
return next(requestContext); 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 { 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 // TODO: Move to a separate Diagnostics folder
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -46,7 +46,7 @@ function _logEntry(entry: Diagnostics.LogEntry): void {
return SeverityLevel.Information; return SeverityLevel.Information;
} }
})(entry.level); })(entry.level);
trackTrace({ message: entry.message, severityLevel }, { area: entry.area }); appInsights.trackTrace({ message: entry.message, severityLevel }, { area: entry.area });
} }
function _generateLogEntry( function _generateLogEntry(

View File

@@ -48,18 +48,32 @@ export function sendCachedDataMessage<TResponseDataModel>(
} }
export function sendMessage(data: any): void { export function sendMessage(data: any): void {
_sendMessage({ 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", signature: "pcIframe",
data: data, data: data,
}); },
portalChildWindow.document.referrer
);
}
} }
export function sendReadyMessage(): void { export function sendReadyMessage(): void {
_sendMessage({ 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", signature: "pcIframe",
kind: "ready", kind: "ready",
data: "ready", data: "ready",
}); },
portalChildWindow.document.referrer
);
}
} }
export function canSendMessage(): boolean { 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

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

View File

@@ -1,15 +1,15 @@
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; 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 { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; 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[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
@@ -17,6 +17,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {
return await readCollectionsWithARM(databaseId); 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 { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels"; import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; 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 { import {
createUpdateCassandraTable, createUpdateCassandraTable,
getCassandraTable, getCassandraTable,
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateGremlinGraph,
getGremlinGraph,
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { import {
createUpdateMongoDBCollection, createUpdateMongoDBCollection,
getMongoDBCollection, getMongoDBCollection,
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; } 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 { import {
ExtendedResourceProperties, createUpdateGremlinGraph,
MongoDBCollectionCreateUpdateParameters, getGremlinGraph,
SqlContainerCreateUpdateParameters, } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
SqlContainerResource, import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function updateCollection( export async function updateCollection(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: Partial<Collection>, newCollection: Collection,
options: RequestOptions = {} options: RequestOptions = {}
): Promise<Collection> { ): Promise<Collection> {
let collection: Collection; let collection: Collection;
@@ -41,6 +43,7 @@ export async function updateCollection(
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.useSDKOperations && !userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
@@ -66,7 +69,7 @@ export async function updateCollection(
async function updateCollectionWithARM( async function updateCollectionWithARM(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
@@ -82,15 +85,6 @@ async function updateCollectionWithARM(
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Table: case DefaultAccountExperienceType.Table:
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
default: default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`); throw new Error(`Unsupported default experience type: ${defaultExperience}`);
} }
@@ -102,7 +96,7 @@ async function updateSqlContainer(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { 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}`); 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, databaseId: string,
collectionId: string, collectionId: string,
subscriptionId: string, newCollection: MongoDBCollectionResource,
resourceGroup: string, updateOptions?: CreateUpdateOptions
accountName: string, ): Promise<MongoDBCollectionResource> {
newCollection: Partial<Collection> const subscriptionId = userContext.subscriptionId;
): Promise<Collection> { const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { 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( const updateResponse = await createUpdateMongoDBCollection(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
databaseId, databaseId,
collectionId, collectionId,
getResponse as MongoDBCollectionCreateUpdateParameters updateParams
); );
return updateResponse && (updateResponse.properties.resource as Collection);
return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource);
} }
throw new Error( throw new Error(
@@ -154,7 +157,7 @@ async function updateCassandraTable(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -181,7 +184,7 @@ async function updateGremlinGraph(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
@@ -205,7 +208,7 @@ async function updateTable(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
newCollection: Partial<Collection> newCollection: Collection
): Promise<Collection> { ): Promise<Collection> {
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId); const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {

View File

@@ -77,6 +77,14 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true); expect(ko.components.isRegistered("delete-collection-confirmation-pane")).toBe(true);
}); });
it("should register 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", () => { it("should register graph-new-vertex-pane component", () => {
expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true); expect(ko.components.isRegistered("graph-new-vertex-pane")).toBe(true);
}); });

View File

@@ -10,6 +10,7 @@ import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleCompo
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent"; import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import * as PaneComponents from "./Panes/PaneComponents"; import * as PaneComponents from "./Panes/PaneComponents";
import ConflictsTab from "./Tabs/ConflictsTab"; import ConflictsTab from "./Tabs/ConflictsTab";
import DatabaseSettingsTab from "./Tabs/DatabaseSettingsTab";
import DocumentsTab from "./Tabs/DocumentsTab"; import DocumentsTab from "./Tabs/DocumentsTab";
import GalleryTab from "./Tabs/GalleryTab"; import GalleryTab from "./Tabs/GalleryTab";
import GraphTab from "./Tabs/GraphTab"; import GraphTab from "./Tabs/GraphTab";
@@ -52,6 +53,7 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
TerminalTab, TerminalTab,
GalleryTab, GalleryTab,
NotebookViewerTab, NotebookViewerTab,
DatabaseSettingsTab,
DatabaseSettingsTabV2, DatabaseSettingsTabV2,
].forEach(({ component: { name, template } }) => ko.components.register(name, { template })); ].forEach(({ component: { name, template } }) => ko.components.register(name, { template }));
@@ -67,7 +69,12 @@ ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVerte
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent()); ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("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("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
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("string-input-pane", new PaneComponents.StringInputPaneComponent()); ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent()); ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent()); ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

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

View File

@@ -1,15 +1,20 @@
import { IButtonProps, IconButton } from "office-ui-fabric-react/lib/Button"; import * as _ from "underscore";
import { ContextualMenu, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"; 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 { import {
DetailsList, DetailsList,
DetailsListLayoutMode, DetailsListLayoutMode,
DetailsRow,
IColumn,
IDetailsListProps, IDetailsListProps,
IDetailsRowProps, IDetailsRowProps,
DetailsRow,
} from "office-ui-fabric-react/lib/DetailsList"; } from "office-ui-fabric-react/lib/DetailsList";
import { FocusZone } from "office-ui-fabric-react/lib/FocusZone"; 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 { import {
IObjectWithKey, IObjectWithKey,
ISelectionZoneProps, ISelectionZoneProps,
@@ -17,18 +22,13 @@ import {
SelectionMode, SelectionMode,
SelectionZone, SelectionZone,
} from "office-ui-fabric-react/lib/utilities/selection/index"; } 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 { StyleConstants } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField";
import { QueriesClient } from "../../../Common/QueriesClient";
import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; 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 { export interface QueriesGridComponentProps {
queriesClient: QueriesClient; 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 { public render(): JSX.Element {
if (this.state.queries.length === 0) { if (this.state.queries.length === 0) {
return this.renderBannerComponent(); return this.renderBannerComponent();
@@ -141,7 +136,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
}, },
}; };
return ( return (
<div id="emptyQueryBanner"> <div>
<div> <div>
You have not saved any queries yet. <br /> <br /> 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 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 container = window.dataExplorer;
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title, paneTitle: container && container.browseQueriesPane.title(),
}); });
try { try {
await this.props.queriesClient.deleteQuery(query); await this.props.queriesClient.deleteQuery(query);
@@ -235,7 +230,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
Action.DeleteSavedQuery, Action.DeleteSavedQuery,
{ {
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title, paneTitle: container && container.browseQueriesPane.title(),
}, },
startKey startKey
); );
@@ -244,7 +239,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
Action.DeleteSavedQuery, Action.DeleteSavedQuery,
{ {
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title, paneTitle: container && container.browseQueriesPane.title(),
error: getErrorMessage(error), error: getErrorMessage(error),
errorStack: getErrorStack(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,10 +1,11 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import ko from "knockout"; import ko from "knockout";
import React from "react"; import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent"; import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
@@ -22,8 +23,13 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
changeFeedPolicy: undefined, changeFeedPolicy: undefined,
analyticalStorageTtl: undefined, analyticalStorageTtl: undefined,
geospatialConfig: undefined, geospatialConfig: undefined,
} as DataModels.Collection),
updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({
id: undefined,
shardKey: undefined,
indexes: [], indexes: [],
}), analyticalStorageTtl: undefined,
} as MongoDBCollectionResource),
})); }));
jest.mock("../../../Common/dataAccess/updateOffer", () => ({ jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
@@ -187,6 +193,7 @@ describe("SettingsComponent", () => {
}; };
await settingsComponentInstance.onSaveClick(); await settingsComponentInstance.onSaveClick();
expect(updateCollection).toBeCalled(); expect(updateCollection).toBeCalled();
expect(updateMongoDBCollectionThroughRP).toBeCalled();
expect(updateOffer).toBeCalled(); expect(updateOffer).toBeCalled();
}); });

View File

@@ -6,7 +6,7 @@ import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; 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 { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
@@ -782,12 +782,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) { if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
try { try {
const newMongoIndexes = this.getMongoIndexesToSave(); const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection = { const newMongoCollection: MongoDBCollectionResource = {
...this.mongoDBCollectionResource, ...this.mongoDBCollectionResource,
indexes: newMongoIndexes, indexes: newMongoIndexes,
}; };
this.mongoDBCollectionResource = await updateCollection( this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.collection.databaseId, this.collection.databaseId,
this.collection.id(), this.collection.id(),
newMongoCollection newMongoCollection

View File

@@ -262,6 +262,52 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -325,6 +371,54 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -519,6 +613,24 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -645,6 +757,7 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -666,6 +779,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -691,6 +805,20 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -719,6 +847,27 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -750,6 +899,22 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -797,6 +962,32 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
@@ -1057,6 +1248,52 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -1120,6 +1357,54 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -1314,6 +1599,24 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -1440,6 +1743,7 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -1461,6 +1765,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -1486,6 +1791,20 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -1514,6 +1833,27 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -1545,6 +1885,22 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -1592,6 +1948,32 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
@@ -1865,6 +2247,52 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -1928,6 +2356,54 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -2122,6 +2598,24 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -2248,6 +2742,7 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -2269,6 +2764,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -2294,6 +2790,20 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -2322,6 +2832,27 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -2353,6 +2884,22 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -2400,6 +2947,32 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
@@ -2660,6 +3233,52 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -2723,6 +3342,54 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -2917,6 +3584,24 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -3043,6 +3728,7 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -3064,6 +3750,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -3089,6 +3776,20 @@ exports[`SettingsComponent renders 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -3117,6 +3818,27 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -3148,6 +3870,22 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -3195,6 +3933,32 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],

View File

@@ -19,12 +19,13 @@ import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { RouteHandler } from "../RouteHandlers/RouteHandler";
import { trackEvent } from "../Shared/appInsights"; import { appInsights } from "../Shared/appInsights";
import * as SharedConstants from "../Shared/Constants"; import * as SharedConstants from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { ExplorerSettings } from "../Shared/ExplorerSettings";
@@ -49,7 +50,7 @@ import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane"; import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { BrowseQueriesPanel } from "./Panes/BrowseQueriesPanel"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
@@ -57,18 +58,18 @@ import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfi
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel"; import { ExecuteSprocParamsPanel } from "./Panes/ExecuteSprocParamsPanel";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPanel } from "./Panes/LoadQueryPanel"; import { LoadQueryPane } from "./Panes/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane"; import NewVertexPane from "./Panes/NewVertexPane";
import { SaveQueryPanel } from "./Panes/SaveQueryPanel"; import { SaveQueryPane } from "./Panes/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane"; import { SettingsPane } from "./Panes/SettingsPane";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; 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 { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase"; import TabsBase from "./Tabs/TabsBase";
@@ -94,10 +95,13 @@ export interface ExplorerParams {
closeSidePanel: () => void; closeSidePanel: () => void;
closeDialog: () => void; closeDialog: () => void;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
tabsManager: TabsManager;
} }
export default class Explorer { export default class Explorer {
public flight: ko.Observable<string> = ko.observable<string>(
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
);
public addCollectionText: ko.Observable<string>; public addCollectionText: ko.Observable<string>;
public addDatabaseText: ko.Observable<string>; public addDatabaseText: ko.Observable<string>;
public collectionTitle: ko.Observable<string>; public collectionTitle: ko.Observable<string>;
@@ -105,6 +109,7 @@ export default class Explorer {
public deleteDatabaseText: ko.Observable<string>; public deleteDatabaseText: ko.Observable<string>;
public collectionTreeNodeAltText: ko.Observable<string>; public collectionTreeNodeAltText: ko.Observable<string>;
public refreshTreeTitle: ko.Observable<string>; public refreshTreeTitle: ko.Observable<string>;
public hasWriteAccess: ko.Observable<boolean>;
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
/** /**
@@ -113,6 +118,11 @@ export default class Explorer {
* */ * */
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>; public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
/**
* @deprecated
* Use userContext.subscriptionType instead
* */
public subscriptionType: ko.Observable<SubscriptionType>;
/** /**
* @deprecated * @deprecated
* Use userContext.apiType instead * Use userContext.apiType instead
@@ -196,8 +206,13 @@ export default class Explorer {
public graphStylingPane: GraphStylingPane; public graphStylingPane: GraphStylingPane;
public addTableEntityPane: AddTableEntityPane; public addTableEntityPane: AddTableEntityPane;
public editTableEntityPane: EditTableEntityPane; public editTableEntityPane: EditTableEntityPane;
public tableColumnOptionsPane: TableColumnOptionsPane;
public querySelectPane: QuerySelectPane;
public newVertexPane: NewVertexPane; public newVertexPane: NewVertexPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public loadQueryPane: LoadQueryPane;
public saveQueryPane: ContextualPaneBase;
public browseQueriesPane: BrowseQueriesPane;
public stringInputPane: StringInputPane; public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane; public setupNotebooksPane: SetupNotebooksPane;
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
@@ -261,6 +276,7 @@ export default class Explorer {
}); });
this.addCollectionText = ko.observable<string>("New Collection"); this.addCollectionText = ko.observable<string>("New Collection");
this.addDatabaseText = ko.observable<string>("New Database"); this.addDatabaseText = ko.observable<string>("New Database");
this.hasWriteAccess = ko.observable<boolean>(true);
this.collectionTitle = ko.observable<string>("Collections"); this.collectionTitle = ko.observable<string>("Collections");
this.collectionTreeNodeAltText = ko.observable<string>("Collection"); this.collectionTreeNodeAltText = ko.observable<string>("Collection");
this.deleteCollectionText = ko.observable<string>("Delete Collection"); this.deleteCollectionText = ko.observable<string>("Delete Collection");
@@ -268,6 +284,7 @@ export default class Explorer {
this.refreshTreeTitle = ko.observable<string>("Refresh collections"); this.refreshTreeTitle = ko.observable<string>("Refresh collections");
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>(); this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.isAccountReady = ko.observable<boolean>(false); this.isAccountReady = ko.observable<boolean>(false);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
@@ -328,7 +345,7 @@ export default class Explorer {
userContext.features.enableSpark userContext.features.enableSpark
); );
if (this.isSparkEnabled()) { if (this.isSparkEnabled()) {
trackEvent( appInsights.trackEvent(
{ name: "LoadedWithSparkEnabled" }, { name: "LoadedWithSparkEnabled" },
{ {
subscriptionId: userContext.subscriptionId, subscriptionId: userContext.subscriptionId,
@@ -566,6 +583,20 @@ export default class Explorer {
container: this, 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({ this.newVertexPane = new NewVertexPane({
id: "newvertexpane", id: "newvertexpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -580,6 +611,27 @@ export default class Explorer {
container: this, container: 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.stringInputPane = new StringInputPane({ this.stringInputPane = new StringInputPane({
id: "stringinputpane", id: "stringinputpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -594,7 +646,7 @@ export default class Explorer {
container: this, container: this,
}); });
this.tabsManager = params?.tabsManager ?? new TabsManager(); this.tabsManager = new TabsManager();
this._panes = [ this._panes = [
this.addDatabasePane, this.addDatabasePane,
@@ -603,8 +655,13 @@ export default class Explorer {
this.graphStylingPane, this.graphStylingPane,
this.addTableEntityPane, this.addTableEntityPane,
this.editTableEntityPane, this.editTableEntityPane,
this.tableColumnOptionsPane,
this.querySelectPane,
this.newVertexPane, this.newVertexPane,
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.loadQueryPane,
this.saveQueryPane,
this.browseQueriesPane,
this.stringInputPane, this.stringInputPane,
this.setupNotebooksPane, this.setupNotebooksPane,
]; ];
@@ -917,8 +974,10 @@ export default class Explorer {
// TODO: Refactor // TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer(); const deferred: Q.Deferred<any> = Q.defer();
this._setLoadingStatusText("Fetching databases...");
readDatabases().then( readDatabases().then(
(databases: DataModels.Database[]) => { (databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabases, Action.LoadDatabases,
{ {
@@ -931,16 +990,20 @@ export default class Explorer {
this.addDatabasesToList(deltaDatabases.toAdd); this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete); this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode); this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
() => { () => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve(); deferred.resolve();
}, },
(reason) => { (reason) => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason); deferred.reject(reason);
} }
); );
}, },
(error) => { (error) => {
this._setLoadingStatusText("Failed to fetch databases.");
deferred.reject(error); deferred.reject(error);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
@@ -1256,6 +1319,11 @@ export default class Explorer {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput; this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
} }
this.databaseAccount(databaseAccount); 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); this.setFeatureFlagsFromFlights(inputs.flights);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
@@ -2212,6 +2280,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 { private _openSetupNotebooksPaneForQuickstart(): void {
const title = "Enable Notebooks (Preview)"; const title = "Enable Notebooks (Preview)";
const description = const description =
@@ -2329,19 +2423,6 @@ 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 { public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.resourceTree.myNotebooksContentRoot;
this.openSidePanel( this.openSidePanel(
@@ -2353,11 +2434,4 @@ export default class Explorer {
/> />
); );
} }
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 { NeighborVertexBasicInfo } from "./GraphExplorer";
import * as GraphData from "./GraphData";
import * as ViewModels from "../../../Contracts/ViewModels";
interface JoinArrayMaxCharOutput { interface JoinArrayMaxCharOutput {
result: string; // string output result: string; // string output
@@ -13,9 +13,9 @@ interface EdgePropertyType {
inV?: string; inV?: string;
} }
export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => { export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`; return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`;
}; }
/** /**
* Collect all edges from this node * Collect all edges from this node
@@ -23,11 +23,11 @@ export const getNeighborTitle = (neighbor: NeighborVertexBasicInfo): string => {
* @param graphData * @param graphData
* @param newNodes (optional) object describing new nodes encountered * @param newNodes (optional) object describing new nodes encountered
*/ */
export const createEdgesfromNode = ( export function createEdgesfromNode(
vertex: GraphData.GremlinVertex, vertex: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>, graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
newNodes?: { [id: string]: boolean } newNodes?: { [id: string]: boolean }
): void => { ): void {
if (Object.prototype.hasOwnProperty.call(vertex, "outE")) { if (Object.prototype.hasOwnProperty.call(vertex, "outE")) {
const outE = vertex.outE; const outE = vertex.outE;
for (const label in outE) { for (const label in outE) {
@@ -66,7 +66,7 @@ export const createEdgesfromNode = (
}); });
} }
} }
}; }
/** /**
* From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'". * From ['id1', 'id2', 'idn'] build the following string "'id1','id2','idn'".
@@ -75,7 +75,7 @@ export const createEdgesfromNode = (
* @param maxSize * @param maxSize
* @return * @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) { if (!array || array.length === 0 || array[0].length + 2 > maxSize) {
return { result: "", consumedCount: 0 }; return { result: "", consumedCount: 0 };
} }
@@ -96,16 +96,16 @@ export const getLimitedArrayString = (array: string[], maxSize: number): JoinArr
result: output, result: output,
consumedCount: i + 1, consumedCount: i + 1,
}; };
}; }
export const createFetchEdgePairQuery = ( export function createFetchEdgePairQuery(
outE: boolean, outE: boolean,
pkid: string, pkid: string,
excludedEdgeIds: string[], excludedEdgeIds: string[],
startIndex: number, startIndex: number,
pageSize: number, pageSize: number,
withoutStepArgMaxLenght: number withoutStepArgMaxLenght: number
): string => { ): string {
let gremlinQuery: string; let gremlinQuery: string;
if (excludedEdgeIds.length > 0) { if (excludedEdgeIds.length > 0) {
// build a string up to max char // build a string up to max char
@@ -128,15 +128,15 @@ export const createFetchEdgePairQuery = (
}().as('v').select('e', 'v')`; }().as('v').select('e', 'v')`;
} }
return gremlinQuery; return gremlinQuery;
}; }
/** /**
* Trim graph * Trim graph
*/ */
export const trimGraph = ( export function trimGraph(
currentRoot: GraphData.GremlinVertex, currentRoot: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
): void => { ) {
const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId); const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId);
graphData.unloadAllVertices(importantNodes); graphData.unloadAllVertices(importantNodes);
@@ -144,32 +144,32 @@ export const trimGraph = (
$.each(graphData.ids, (index: number, id: string) => { $.each(graphData.ids, (index: number, id: string) => {
graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1; graphData.getVertexById(id)._isFixedPosition = importantNodes.indexOf(id) !== -1;
}); });
}; }
export const addRootChildToGraph = ( export function addRootChildToGraph(
root: GraphData.GremlinVertex, root: GraphData.GremlinVertex,
child: GraphData.GremlinVertex, child: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
): void => { ) {
child._ancestorsId = (root._ancestorsId || []).concat([root.id]); child._ancestorsId = (root._ancestorsId || []).concat([root.id]);
graphData.addVertex(child); graphData.addVertex(child);
createEdgesfromNode(child, graphData); createEdgesfromNode(child, graphData);
graphData.addNeighborInfo(child); graphData.addNeighborInfo(child);
}; }
/** /**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now.
* @param value * @param value
*/ */
export const escapeDoubleQuotes = (value: string): string => { export function escapeDoubleQuotes(value: string): string {
return value === undefined ? value : value.replace(/"/g, '\\"'); return value === undefined ? value : value.replace(/"/g, '\\"');
}; }
/** /**
* Surround with double-quotes if val is a string. * Surround with double-quotes if val is a string.
* @param val * @param val
*/ */
export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string => { export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
switch (ip.type) { switch (ip.type) {
case "number": case "number":
case "boolean": case "boolean":
@@ -179,12 +179,12 @@ export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string =>
default: default:
return `"${escapeDoubleQuotes(ip.value as string)}"`; return `"${escapeDoubleQuotes(ip.value as string)}"`;
} }
}; }
/** /**
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now.
* @param value * @param value
*/ */
export const escapeSingleQuotes = (value: string): string => { export function escapeSingleQuotes(value: string): string {
return value === undefined ? value : value.replace(/'/g, "\\'"); return value === undefined ? value : value.replace(/'/g, "\\'");
}; }

View File

@@ -420,7 +420,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
return { return {
iconSrc: BrowseQueriesIcon, iconSrc: BrowseQueriesIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.openBrowseQueriesPanel(), onCommandClick: () => container.browseQueriesPane.open(),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
@@ -433,7 +433,7 @@ function createOpenQueryFromDiskButton(container: Explorer): CommandButtonCompon
return { return {
iconSrc: OpenQueryFromDiskIcon, iconSrc: OpenQueryFromDiskIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => container.openLoadQueryPanel(), onCommandClick: () => container.loadQueryPane.open(),
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,

View File

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

View File

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

@@ -4,14 +4,11 @@
import { import {
actions, actions,
AppState, AppState,
ContentRecord, ContentRecord, createHostRef,
createHostRef,
createKernelspecsRef, createKernelspecsRef,
HostRecord, HostRecord,
HostRef, HostRef,
IContentProvider, IContentProvider, KernelspecsRef, makeAppRecord,
KernelspecsRef,
makeAppRecord,
makeCommsRecord, makeCommsRecord,
makeContentsRecord, makeContentsRecord,
makeEditorsRecord, makeEditorsRecord,
@@ -19,7 +16,7 @@ import {
makeHostsRecord, makeHostsRecord,
makeJupyterHostRecord, makeJupyterHostRecord,
makeStateRecord, makeStateRecord,
makeTransformsRecord, makeTransformsRecord
} from "@nteract/core"; } from "@nteract/core";
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration"; import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
import { Media } from "@nteract/outputs"; import { Media } from "@nteract/outputs";
@@ -31,10 +28,10 @@ import * as Constants from "../../Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels"; import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import configureStore from "./NotebookComponent/store"; import configureStore from "./NotebookComponent/store";
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types"; import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
import JavaScript from "./NotebookRenderer/outputs/javascript"; import IFrameHTML from "./NotebookRenderer/outputs/IFrameHTML";
import IFrameJavaScript from "./NotebookRenderer/outputs/IFrameJavaScript";
export type KernelSpecsDisplay = { name: string; displayName: string }; export type KernelSpecsDisplay = { name: string; displayName: string };
@@ -167,8 +164,8 @@ export class NotebookClientV2 {
"application/vnd.vega.v5+json": NullTransform, "application/vnd.vega.v5+json": NullTransform,
"application/vdom.v1+json": TransformVDOM, "application/vdom.v1+json": TransformVDOM,
"application/json": Media.Json, "application/json": Media.Json,
"application/javascript": userContext.features.sandboxNotebookOutputs ? JavaScript : Media.JavaScript, "application/javascript": IFrameJavaScript,
"text/html": Media.HTML, "text/html": IFrameHTML,
"text/markdown": Media.Markdown, "text/markdown": Media.Markdown,
"text/latex": Media.LaTeX, "text/latex": Media.LaTeX,
"image/svg+xml": Media.SVG, "image/svg+xml": Media.SVG,

View File

@@ -1,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 * 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 "./base.css";
import "./default.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 "./NotebookReadOnlyRenderer.less";
import IFrameOutputs from "./outputs/IFrameOutputs";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: any;
@@ -62,16 +60,6 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<CodeCell id={id} contentRef={contentRef}> <CodeCell id={id} contentRef={contentRef}>
{{ {{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef), prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
outputs: userContext.features.sandboxNotebookOutputs
? (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: { editor: {
monaco: (props: PassedEditorProps) => monaco: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />, this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,

View File

@@ -1,32 +1,37 @@
import { CellId } from "@nteract/commutable"; import * as React from "react";
import { CellType } from "@nteract/commutable/src"; import "./base.css";
import { actions, ContentRef } from "@nteract/core"; import "./default.css";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components"; import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import * as React from "react";
import { 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 Prompt from "./Prompt";
import { promptContent } from "./PromptContent"; 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 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 { export interface NotebookRendererBaseProps {
contentRef: any; contentRef: any;
@@ -107,16 +112,6 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
</Prompt> </Prompt>
), ),
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />, 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> </CodeCell>
), ),

View File

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

View File

@@ -1,5 +1,5 @@
import { Media } from "@nteract/outputs";
import React from "react"; import React from "react";
import IFrameHTML from "./IFrameHTML";
interface Props { interface Props {
/** /**
@@ -12,15 +12,17 @@ interface Props {
mediaType: "text/javascript"; mediaType: "text/javascript";
} }
export class JavaScript extends React.PureComponent<Props> { export class IFrameJavaScript extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
data: "", data: "",
mediaType: "application/javascript", mediaType: "application/javascript"
}; };
render(): JSX.Element { render() {
return <Media.HTML data={`<script>${this.props.data}</script>`} />; return (
<IFrameHTML data={`<script>${this.props.data}</script>`} />
);
} }
} }
export default JavaScript; export default IFrameJavaScript;

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

@@ -105,6 +105,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseId = ko.observable<string>(); this.databaseId = ko.observable<string>();
this.databaseCreateNew = ko.observable<boolean>(true); this.databaseCreateNew = ko.observable<boolean>(true);
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault()); 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.collectionWithThroughputInShared = ko.observable<boolean>(false);
this.databaseIds = ko.observableArray<string>(); this.databaseIds = ko.observableArray<string>();
this.uniqueKeys = ko.observableArray<DynamicListItem>(); this.uniqueKeys = ko.observableArray<DynamicListItem>();
@@ -474,6 +478,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.resetData(); this.resetData();
this.container.flight.subscribe(() => {
this.resetData();
});
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() => this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
this.isFreeTierAccount() && !this.container.isFirstResourceCreated() this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
@@ -652,7 +659,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
public getSharedThroughputDefault(): boolean { public getSharedThroughputDefault(): boolean {
const subscriptionType = userContext.subscriptionType; const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) { if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false; return false;
} }
@@ -694,12 +701,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
partitionKey: this.partitionKey(), partitionKey: this.partitionKey(),
databaseId: this.databaseId(), databaseId: this.databaseId(),
}), }),
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: this._getThroughput(), throughput: this._getThroughput(),
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
}; };
@@ -798,12 +805,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared(), collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
}), }),
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
useIndexingForSharedThroughput: this.useIndexingForSharedThroughput(), useIndexingForSharedThroughput: this.useIndexingForSharedThroughput(),
@@ -870,12 +877,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared(), collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
}), }),
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
}; };
@@ -902,12 +909,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared(), collectionWithThroughputInShared: this.collectionWithThroughputInShared(),
}, },
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage, error: errorMessage,

View File

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

View File

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

@@ -5,6 +5,7 @@ import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"
import { HashMap } from "../../Common/HashMap"; import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility"; import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as SharedConstants from "../../Shared/Constants"; import * as SharedConstants from "../../Shared/Constants";
@@ -116,6 +117,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.resetData(); this.resetData();
this.container.flight.subscribe(() => {
this.resetData();
});
this.requestUnitsUsageCostDedicated = ko.computed(() => { this.requestUnitsUsageCostDedicated = ko.computed(() => {
const account = this.container.databaseAccount(); const account = this.container.databaseAccount();
if (!account) { if (!account) {
@@ -301,12 +306,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId(), databaseId: this.keyspaceId(),
}), }),
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
}; };
@@ -353,12 +358,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput(), hasDedicatedThroughput: this.dedicateTableThroughput(),
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace, toCreateKeyspace: toCreateKeyspace,
@@ -397,12 +402,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput(), hasDedicatedThroughput: this.dedicateTableThroughput(),
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace, toCreateKeyspace: toCreateKeyspace,
@@ -425,12 +430,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput(), hasDedicatedThroughput: this.dedicateTableThroughput(),
}, },
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType, subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
flight: userContext.addCollectionFlight, flight: this.container.flight(),
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace, toCreateKeyspace: toCreateKeyspace,

View File

@@ -1061,7 +1061,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<button <button
aria-label="Close pane" aria-label="Close pane"
className="ms-Button ms-Button--icon closePaneBtn root-40" className="ms-Button ms-Button--icon closePaneBtn root-72"
data-is-focusable={true} data-is-focusable={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -1074,16 +1074,16 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-41" className="ms-Button-flexContainer flexContainer-73"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<Component <Component
className="ms-Button-icon icon-43" className="ms-Button-icon icon-75"
iconName="Cancel" iconName="Cancel"
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Icon root-37 css-48 ms-Button-icon icon-43" className="ms-Icon root-37 css-80 ms-Button-icon icon-75"
data-icon-name="Cancel" data-icon-name="Cancel"
role="presentation" role="presentation"
style={ style={
@@ -1429,7 +1429,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-49" className="ms-Label root-81"
> >
Partition key value Partition key value
</label> </label>
@@ -1439,7 +1439,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<div <div
className="ms-Stack css-50" className="ms-Stack css-82"
> >
<StyledWithResponsiveMode <StyledWithResponsiveMode
key=".0:$.0" key=".0:$.0"
@@ -2336,7 +2336,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label ms-Dropdown-label root-67" className="ms-Label ms-Dropdown-label root-99"
id="Dropdown3-label" id="Dropdown3-label"
> >
Key Key
@@ -2348,7 +2348,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="listbox" aria-haspopup="listbox"
aria-labelledby="Dropdown3-label Dropdown3-option" aria-labelledby="Dropdown3-label Dropdown3-option"
className="ms-Dropdown dropdown-51" className="ms-Dropdown dropdown-83"
data-is-focusable={true} data-is-focusable={true}
id="Dropdown3" id="Dropdown3"
onBlur={[Function]} onBlur={[Function]}
@@ -2368,23 +2368,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-posinset={1} aria-posinset={1}
aria-selected={true} aria-selected={true}
aria-setsize={2} aria-setsize={2}
className="ms-Dropdown-title title-52" className="ms-Dropdown-title title-84"
id="Dropdown3-option" id="Dropdown3-option"
role="option" role="option"
> >
String String
</span> </span>
<span <span
className="ms-Dropdown-caretDownWrapper caretDownWrapper-53" className="ms-Dropdown-caretDownWrapper caretDownWrapper-85"
> >
<StyledIconBase <StyledIconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-54" className="ms-Dropdown-caretDown caretDown-86"
iconName="ChevronDown" iconName="ChevronDown"
> >
<IconBase <IconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-54" className="ms-Dropdown-caretDown caretDown-86"
iconName="ChevronDown" iconName="ChevronDown"
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -2663,7 +2663,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-68" className="ms-Dropdown-caretDown caretDown-100"
data-icon-name="ChevronDown" data-icon-name="ChevronDown"
> >
@@ -2971,7 +2971,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
value="" value=""
> >
<div <div
className="ms-TextField root-70" className="ms-TextField root-102"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@@ -3260,7 +3260,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-49" className="ms-Label root-81"
htmlFor="confirmCollectionId" htmlFor="confirmCollectionId"
id="TextFieldLabel6" id="TextFieldLabel6"
> >
@@ -3269,13 +3269,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</LabelBase> </LabelBase>
</StyledLabelBase> </StyledLabelBase>
<div <div
className="ms-TextField-fieldGroup fieldGroup-71" className="ms-TextField-fieldGroup fieldGroup-103"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-labelledby="TextFieldLabel6" aria-labelledby="TextFieldLabel6"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-72" className="ms-TextField-field field-104"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -3583,7 +3583,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-49" className="ms-Label root-81"
> >
Enter input parameters (if any) Enter input parameters (if any)
</label> </label>
@@ -3593,7 +3593,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<div <div
className="ms-Stack css-50" className="ms-Stack css-82"
> >
<StyledWithResponsiveMode <StyledWithResponsiveMode
key=".0:$.0" key=".0:$.0"
@@ -4490,7 +4490,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label ms-Dropdown-label root-67" className="ms-Label ms-Dropdown-label root-99"
id="Dropdown7-label" id="Dropdown7-label"
> >
Key Key
@@ -4502,7 +4502,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-expanded="false" aria-expanded="false"
aria-haspopup="listbox" aria-haspopup="listbox"
aria-labelledby="Dropdown7-label Dropdown7-option" aria-labelledby="Dropdown7-label Dropdown7-option"
className="ms-Dropdown dropdown-51" className="ms-Dropdown dropdown-83"
data-is-focusable={true} data-is-focusable={true}
id="Dropdown7" id="Dropdown7"
onBlur={[Function]} onBlur={[Function]}
@@ -4522,23 +4522,23 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-posinset={1} aria-posinset={1}
aria-selected={true} aria-selected={true}
aria-setsize={2} aria-setsize={2}
className="ms-Dropdown-title title-52" className="ms-Dropdown-title title-84"
id="Dropdown7-option" id="Dropdown7-option"
role="option" role="option"
> >
String String
</span> </span>
<span <span
className="ms-Dropdown-caretDownWrapper caretDownWrapper-53" className="ms-Dropdown-caretDownWrapper caretDownWrapper-85"
> >
<StyledIconBase <StyledIconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-54" className="ms-Dropdown-caretDown caretDown-86"
iconName="ChevronDown" iconName="ChevronDown"
> >
<IconBase <IconBase
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-54" className="ms-Dropdown-caretDown caretDown-86"
iconName="ChevronDown" iconName="ChevronDown"
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -4817,7 +4817,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="ms-Dropdown-caretDown caretDown-68" className="ms-Dropdown-caretDown caretDown-100"
data-icon-name="ChevronDown" data-icon-name="ChevronDown"
> >
@@ -5125,7 +5125,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
value="" value=""
> >
<div <div
className="ms-TextField root-70" className="ms-TextField root-102"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@@ -5414,7 +5414,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
} }
> >
<label <label
className="ms-Label root-49" className="ms-Label root-81"
htmlFor="confirmCollectionId" htmlFor="confirmCollectionId"
id="TextFieldLabel10" id="TextFieldLabel10"
> >
@@ -5423,13 +5423,13 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</LabelBase> </LabelBase>
</StyledLabelBase> </StyledLabelBase>
<div <div
className="ms-TextField-fieldGroup fieldGroup-71" className="ms-TextField-fieldGroup fieldGroup-103"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-labelledby="TextFieldLabel10" aria-labelledby="TextFieldLabel10"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-72" className="ms-TextField-field field-104"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -5737,7 +5737,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
width={20} width={20}
> >
<div <div
className="ms-Image addRemoveIconLabel root-81" className="ms-Image addRemoveIconLabel root-113"
style={ style={
Object { Object {
"height": 30, "height": 30,
@@ -5747,7 +5747,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<img <img
alt="Delete param" alt="Delete param"
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-82" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-114"
id="deleteparam" id="deleteparam"
key="fabricImage" key="fabricImage"
onClick={[Function]} onClick={[Function]}
@@ -6052,7 +6052,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
width={20} width={20}
> >
<div <div
className="ms-Image addRemoveIconLabel root-81" className="ms-Image addRemoveIconLabel root-113"
style={ style={
Object { Object {
"height": 30, "height": 30,
@@ -6062,7 +6062,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<img <img
alt="Add param" alt="Add param"
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-82" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-114"
id="addparam" id="addparam"
key="fabricImage" key="fabricImage"
onClick={[Function]} onClick={[Function]}
@@ -6081,7 +6081,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
onClick={[Function]} onClick={[Function]}
> >
<div <div
className="ms-Stack css-50" className="ms-Stack css-82"
onClick={[Function]} onClick={[Function]}
> >
<StyledImageBase <StyledImageBase
@@ -6373,7 +6373,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
width={20} width={20}
> >
<div <div
className="ms-Image root-81" className="ms-Image root-113"
style={ style={
Object { Object {
"height": 30, "height": 30,
@@ -6383,7 +6383,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<img <img
alt="Add param" alt="Add param"
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-82" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-114"
key="fabricImage" key="fabricImage"
onError={[Function]} onError={[Function]}
onLoad={[Function]} onLoad={[Function]}
@@ -6397,7 +6397,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
key=".0:$.1" key=".0:$.1"
> >
<span <span
className="addNewParamStyle css-83" className="addNewParamStyle css-115"
> >
Add New Param Add New Param
</span> </span>
@@ -8123,7 +8123,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<button <button
aria-label="Submit" aria-label="Submit"
className="ms-Button ms-Button--primary genericPaneSubmitBtn root-84" className="ms-Button ms-Button--primary genericPaneSubmitBtn root-116"
data-is-focusable={true} data-is-focusable={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -8141,14 +8141,14 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
type="button" type="button"
> >
<span <span
className="ms-Button-flexContainer flexContainer-41" className="ms-Button-flexContainer flexContainer-73"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<span <span
className="ms-Button-textContainer textContainer-42" className="ms-Button-textContainer textContainer-74"
> >
<span <span
className="ms-Button-label label-85" className="ms-Button-label label-117"
id="id__11" id="id__11"
key="id__11" key="id__11"
> >

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,19 @@
import AddCollectionPaneTemplate from "./AddCollectionPane.html"; import AddCollectionPaneTemplate from "./AddCollectionPane.html";
import AddDatabasePaneTemplate from "./AddDatabasePane.html"; import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import BrowseQueriesPaneTemplate from "./BrowseQueriesPane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html"; import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html"; import DeleteCollectionConfirmationPaneTemplate from "./DeleteCollectionConfirmationPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html"; import GitHubReposPaneTemplate from "./GitHubReposPane.html";
import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html"; import GraphNewVertexPaneTemplate from "./GraphNewVertexPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html"; import GraphStylingPaneTemplate from "./GraphStylingPane.html";
import LoadQueryPaneTemplate from "./LoadQueryPane.html";
import SaveQueryPaneTemplate from "./SaveQueryPane.html";
import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html"; import SetupNotebooksPaneTemplate from "./SetupNotebooksPane.html";
import StringInputPaneTemplate from "./StringInputPane.html"; import StringInputPaneTemplate from "./StringInputPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html"; import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableColumnOptionsPaneTemplate from "./Tables/TableColumnOptionsPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html"; import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
import TableQuerySelectPaneTemplate from "./Tables/TableQuerySelectPane.html";
export class PaneComponent { export class PaneComponent {
constructor(data: any) { constructor(data: any) {
@@ -78,6 +83,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 { export class CassandraAddCollectionPaneComponent {
constructor() { constructor() {
return { return {
@@ -87,6 +111,33 @@ export class CassandraAddCollectionPaneComponent {
} }
} }
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 StringInputPaneComponent { export class StringInputPaneComponent {
constructor() { constructor() {
return { return {

View File

@@ -126,17 +126,6 @@
.panelGroupSpacing > * { .panelGroupSpacing > * {
margin-bottom: @SmallSpace; margin-bottom: @SmallSpace;
} }
.fileUpload {
display: none !important;
}
.customFileUpload {
padding: 25px 0px 0px 10px;
cursor: pointer;
display: flex;
}
.fileIcon {
align-self: center;
}
.panelAddIconLabel { .panelAddIconLabel {
font-size: 20px; font-size: 20px;
width: 20px; width: 20px;
@@ -152,6 +141,3 @@
.removeIcon { .removeIcon {
color: @InfoIconColor; color: @InfoIconColor;
} }
.column-select-view {
margin: 20px 0px 0px 0px;
}

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

@@ -238,6 +238,52 @@ exports[`Settings Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -301,6 +347,54 @@ exports[`Settings Pane should render Default properly 1`] = `
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -495,6 +589,24 @@ exports[`Settings Pane should render Default properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -621,6 +733,7 @@ exports[`Settings Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -642,6 +755,7 @@ exports[`Settings Pane should render Default properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -667,6 +781,20 @@ exports[`Settings Pane should render Default properly 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -695,6 +823,27 @@ exports[`Settings Pane should render Default properly 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -726,6 +875,22 @@ exports[`Settings Pane should render Default properly 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -773,6 +938,32 @@ exports[`Settings Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
@@ -1121,6 +1312,52 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -1184,6 +1421,54 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -1378,6 +1663,24 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -1504,6 +1807,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -1525,6 +1829,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -1550,6 +1855,20 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -1578,6 +1897,27 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -1609,6 +1949,22 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -1656,6 +2012,32 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],

View File

@@ -0,0 +1,174 @@
import * as ko from "knockout";
import _ from "underscore";
import * as Constants from "../../Tables/Constants";
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ContextualPaneBase } from "../ContextualPaneBase";
export interface ISelectColumn {
columnName: ko.Observable<string>;
selected: ko.Observable<boolean>;
editable: ko.Observable<boolean>;
}
export class QuerySelectPane extends ContextualPaneBase {
public titleLabel: string = "Select Columns";
public instructionLabel: string = "Select the columns that you want to query.";
public availableColumnsTableQueryLabel: string = "Available Columns";
public noColumnSelectedWarning: string = "At least one column should be selected.";
public columnOptions: ko.ObservableArray<ISelectColumn>;
public anyColumnSelected: ko.Computed<boolean>;
public canSelectAll: ko.Computed<boolean>;
public allSelected: ko.Computed<boolean>;
private selectedColumnOption: ISelectColumn = null;
public queryViewModel: QueryViewModel;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.columnOptions = ko.observableArray<ISelectColumn>();
this.anyColumnSelected = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return value.selected();
});
});
this.canSelectAll = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return !value.selected();
});
});
this.allSelected = ko.pureComputed<boolean>({
read: () => {
return !this.canSelectAll();
},
write: (value) => {
if (value) {
this.selectAll();
} else {
this.clearAll();
}
},
owner: this,
});
}
public submit() {
this.queryViewModel.selectText(this.getParameters());
this.queryViewModel.getSelectMessage();
this.close();
}
public open() {
this.setTableColumns(this.queryViewModel.columnOptions());
this.setDisplayedColumns(this.queryViewModel.selectText(), this.columnOptions());
super.open();
}
private getParameters(): string[] {
if (this.canSelectAll() === false) {
return [];
}
var selectedColumns = this.columnOptions().filter((value: ISelectColumn) => value.selected() === true);
var columns: string[] = selectedColumns.map((value: ISelectColumn) => {
var name: string = value.columnName();
return name;
});
return columns;
}
public setTableColumns(columnNames: string[]): void {
var columns: ISelectColumn[] = columnNames.map((value: string) => {
var columnOption: ISelectColumn = {
columnName: ko.observable<string>(value),
selected: ko.observable<boolean>(true),
editable: ko.observable<boolean>(this.isEntityEditable(value)),
};
return columnOption;
});
this.columnOptions(columns);
}
public setDisplayedColumns(querySelect: string[], columns: ISelectColumn[]): void {
if (querySelect == null || _.isEmpty(querySelect)) {
return;
}
this.setSelected(querySelect, columns);
}
private setSelected(querySelect: string[], columns: ISelectColumn[]): void {
this.clearAll();
querySelect &&
querySelect.forEach((value: string) => {
for (var i = 0; i < columns.length; i++) {
if (value === columns[i].columnName()) {
columns[i].selected(true);
}
}
});
}
public availableColumnsCheckboxClick(): boolean {
if (this.canSelectAll()) {
return this.selectAll();
} else {
return this.clearAll();
}
}
public selectAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: ISelectColumn) => {
value.selected(true);
});
return true;
}
public clearAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((column: ISelectColumn) => {
if (this.isEntityEditable(column.columnName())) {
column.selected(false);
} else {
column.selected(true);
}
});
return true;
}
public handleClick = (data: ISelectColumn, event: KeyboardEvent): boolean => {
this.selectTargetItem($(event.currentTarget), data);
return true;
};
private selectTargetItem($target: JQuery, targetColumn: ISelectColumn): void {
this.selectedColumnOption = targetColumn;
$(".list-item.selected").removeClass("selected");
$target.addClass("selected");
}
private isEntityEditable(name: string) {
if (this.queryViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
const cassandraKeys = this.queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
return !_.contains<string>(cassandraKeys, name);
}
return !(
name === Constants.EntityKeyNames.PartitionKey ||
name === Constants.EntityKeyNames.RowKey ||
name === Constants.EntityKeyNames.Timestamp
);
}
}

View File

@@ -0,0 +1,78 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="tablecolumnoptionspane">
<!-- Table Column Options form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Table Column Options header - Start -->
<div class="firstdivbg headerline">
Column Options
<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>
<!-- Table Column Options header - End -->
<div class="paneMainContent paneContentContainer">
<div><span>Choose the columns and the order in which you want to display them in the table.</span></div>
<div class="column-options">
<div class="columns-border">
<input class="all-select-check" type="checkbox" data-bind="checked: allSelected" />
<label
style="font-weight: 700"
id="availableColumnsLabel"
data-bind="text: availableColumnsLabel"
></label>
<span class="column-arrows-svg" data-bind="click: moveDown, enable: canMoveDown">
<img class="column-opt-arrow-Img" src="/Down.svg" alt="Move down" />
</span>
<span class="column-arrows-svg" data-bind="click: moveUp, enable: canMoveUp">
<img class="column-opt-arrow-Img" src="/Up.svg" alt="Move up" />
</span>
</div>
<div class="content">
<section>
<ul data-bind="foreach: columnOptions" aria-labelledby="availableColumnsLabel" tabindex="0">
<li
class="list-item columns-border"
data-bind="attr: { title: columnName }, click: $parent.handleClick "
>
<input
type="checkbox"
for="columnName"
data-bind="attr: { title: columnName, 'aria-selected': (selected()? 'true': 'false') }, checked: selected"
/>
<label id="columnName" data-bind="text: columnName"></label>
</li>
</ul>
</section>
</div>
</div>
<div class="row-label" data-bind="style: { visibility: anyColumnSelected() ? 'hidden': 'visible' }">
<label class="warning" role="alert" aria-atomic="true" data-bind="text: noColumnSelectedWarning"></label>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Table Column Options form - End -->
</div>
</div>

View File

@@ -0,0 +1,195 @@
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataTableOperations from "../../Tables/DataTable/DataTableOperations";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import { ContextualPaneBase } from "../ContextualPaneBase";
import _ from "underscore";
/**
* Represents an item shown in the available columns.
* columnName: the name of the column.
* selected: indicate whether user wants to display the column in the table.
* order: the order in the initial table. E.g.,
* Order array of initial table: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Order array of current table: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
* if order = 6, then this column will be the one with column name prop6
* index: index in the observable array, this used for selection and moving up/down.
*/
interface IColumnOption {
columnName: ko.Observable<string>;
selected: ko.Observable<boolean>;
order: number;
index: number;
}
export interface IColumnSetting {
columnNames: string[];
visible?: boolean[];
order?: number[];
}
export class TableColumnOptionsPane extends ContextualPaneBase {
public titleLabel: string = "Column Options";
public instructionLabel: string = "Choose the columns and the order in which you want to display them in the table.";
public availableColumnsLabel: string = "Available Columns";
public moveUpLabel: string = "Move Up";
public moveDownLabel: string = "Move Down";
public noColumnSelectedWarning: string = "At least one column should be selected.";
public columnOptions: ko.ObservableArray<IColumnOption>;
public allSelected: ko.Computed<boolean>;
public anyColumnSelected: ko.Computed<boolean>;
public canSelectAll: ko.Computed<boolean>;
public canMoveUp: ko.Observable<boolean>;
public canMoveDown: ko.Observable<boolean>;
public tableViewModel: TableEntityListViewModel;
public parameters: IColumnSetting;
private selectedColumnOption: IColumnOption = null;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.columnOptions = ko.observableArray<IColumnOption>();
this.anyColumnSelected = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: IColumnOption) => {
return value.selected();
});
});
this.canSelectAll = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: IColumnOption) => {
return !value.selected();
});
});
this.canMoveUp = ko.observable<boolean>(false);
this.canMoveDown = ko.observable<boolean>(false);
this.allSelected = ko.pureComputed<boolean>({
read: () => {
return !this.canSelectAll();
},
write: (value) => {
if (value) {
this.selectAll();
} else {
this.clearAll();
}
},
owner: this,
});
}
public submit() {
var newColumnSetting = this.getParameters();
DataTableOperations.reorderColumns(this.tableViewModel.table, newColumnSetting.order).then(() => {
DataTableOperations.filterColumns(this.tableViewModel.table, newColumnSetting.visible);
this.visible(false);
});
}
public open() {
this.setDisplayedColumns(this.parameters.columnNames, this.parameters.order, this.parameters.visible);
super.open();
}
private getParameters(): IColumnSetting {
var newColumnSettings: IColumnSetting = <IColumnSetting>{
columnNames: [],
order: [],
visible: [],
};
this.columnOptions().map((value: IColumnOption) => {
newColumnSettings.columnNames.push(value.columnName());
newColumnSettings.order.push(value.order);
newColumnSettings.visible.push(value.selected());
});
return newColumnSettings;
}
public setDisplayedColumns(columnNames: string[], order: number[], visible: boolean[]): void {
var options: IColumnOption[] = order.map((value: number, index: number) => {
var columnOption: IColumnOption = {
columnName: ko.observable<string>(columnNames[index]),
order: value,
selected: ko.observable<boolean>(visible[index]),
index: index,
};
return columnOption;
});
this.columnOptions(options);
}
public selectAll(): void {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: IColumnOption) => {
value.selected(true);
});
}
public clearAll(): void {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: IColumnOption) => {
value.selected(false);
});
if (columnOptions && columnOptions.length > 0) {
columnOptions[0].selected(true);
}
}
public moveUp(): void {
if (this.selectedColumnOption) {
var currentSelectedIndex: number = this.selectedColumnOption.index;
var swapTargetIndex: number = currentSelectedIndex - 1;
//Debug.assert(currentSelectedIndex > 0);
this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex);
this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]);
}
}
public moveDown(): void {
if (this.selectedColumnOption) {
var currentSelectedIndex: number = this.selectedColumnOption.index;
var swapTargetIndex: number = currentSelectedIndex + 1;
//Debug.assert(currentSelectedIndex < (this.columnOptions().length - 1));
this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex);
this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]);
}
}
public handleClick = (data: IColumnOption, event: KeyboardEvent): boolean => {
this.selectTargetItem($(event.currentTarget), data);
return true;
};
private selectTargetItem($target: JQuery, targetColumn: IColumnOption): void {
this.selectedColumnOption = targetColumn;
this.canMoveUp(targetColumn.index !== 0);
this.canMoveDown(targetColumn.index !== this.columnOptions().length - 1);
$(".list-item.selected").removeClass("selected");
$target.addClass("selected");
}
private swapColumnOption(options: IColumnOption[], indexA: number, indexB: number): void {
var tempColumnName: string = options[indexA].columnName();
var tempSelected: boolean = options[indexA].selected();
var tempOrder: number = options[indexA].order;
options[indexA].columnName(options[indexB].columnName());
options[indexB].columnName(tempColumnName);
options[indexA].selected(options[indexB].selected());
options[indexB].selected(tempSelected);
options[indexA].order = options[indexB].order;
options[indexB].order = tempOrder;
}
}

View File

@@ -0,0 +1,79 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="queryselectpane">
<!-- Query Select form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Query Select header - Start -->
<div class="firstdivbg headerline">
Select Column
<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>
<!-- Query Select header - End -->
<div class="paneMainContent paneContentContainer">
<!--<div class="row">
<label id="instructionLabel" data-bind="text: instructionLabel"></label>
</div>-->
<div><span>Select the columns that you want to query.</span></div>
<div class="column-options">
<div class="columns-border">
<input class="all-select-check" type="checkbox" data-bind="checked: allSelected" />
<label
style="font-weight: 700"
id="availableColumnsTableQueryLabel"
data-bind="text: availableColumnsTableQueryLabel"
></label>
</div>
<div class="content">
<section>
<ul data-bind="foreach: columnOptions" aria-labelledby="availableColumnsTableQueryLabel" tabindex="0">
<!-- ko template: {if: editable} -->
<li
class="list-item columns-border"
data-bind="attr: { title: columnName }, click: $parent.handleClick "
>
<input type="checkbox" data-bind="attr: { title: columnName }, checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
<!-- ko template: {ifnot: editable} -->
<li class="list-item columns-border" data-bind="attr: { title: columnName } ">
<input type="checkbox" disabled data-bind="checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
</ul>
</section>
</div>
</div>
<div class="row-label" data-bind="style: { visibility: anyColumnSelected() ? 'hidden': 'visible' }">
<label class="warning" role="alert" aria-atomic="true" data-bind="text: noColumnSelectedWarning"></label>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Query Select form - End -->
</div>
</div>

View File

@@ -1,38 +0,0 @@
import { mount } from "enzyme";
import * as ko from "knockout";
import React from "react";
import Explorer from "../../../Explorer";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { TableQuerySelectPanel } from "./index";
describe("Table query select Panel", () => {
const fakeExplorer = {} as Explorer;
const fakeQueryViewModal = {} as QueryViewModel;
fakeQueryViewModal.columnOptions = ko.observableArray<string>([""]);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
queryViewModel: fakeQueryViewModal,
};
it("should render Default properly", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("Should exist availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.exists("#availableCheckbox")).toBe(true);
});
it("Should checked availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
id: "availableCheckbox",
label: "Available Columns",
checked: true,
onChange: expect.any(Function),
});
});
});

View File

@@ -1,155 +0,0 @@
import { Checkbox, Text } from "office-ui-fabric-react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer";
import * as Constants from "../../../Tables/Constants";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../../GenericRightPaneComponent";
interface TableQuerySelectPanelProps {
explorer: Explorer;
closePanel: () => void;
queryViewModel: QueryViewModel;
}
interface ISelectColumn {
columnName: string;
selected: boolean;
editable: boolean;
}
export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps> = ({
explorer,
closePanel,
queryViewModel,
}: TableQuerySelectPanelProps): JSX.Element => {
const [columnOptions, setColumnOptions] = useState<ISelectColumn[]>([
{ columnName: "", selected: true, editable: false },
]);
const [isAvailableColumnChecked, setIsAvailableColumnChecked] = useState<boolean>(true);
const genericPaneProps: GenericRightPaneProps = {
container: explorer,
formError: "",
formErrorDetail: "",
id: "querySelectPane",
isExecuting: false,
title: "Select Column",
submitButtonText: "OK",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const submit = (): void => {
queryViewModel.selectText(getParameters());
queryViewModel.getSelectMessage();
closePanel();
};
const handleClick = (isChecked: boolean, selectedColumn: string): void => {
const columns = columnOptions.map((column) => {
if (column.columnName === selectedColumn) {
column.selected = isChecked;
return { ...column };
}
return { ...column };
});
canSelectAll();
setColumnOptions(columns);
};
useEffect(() => {
queryViewModel && setTableColumns(queryViewModel.columnOptions());
}, []);
const setTableColumns = (columnNames: string[]): void => {
const columns: ISelectColumn[] =
columnNames &&
columnNames.length &&
columnNames.map((value: string) => {
const columnOption: ISelectColumn = {
columnName: value,
selected: true,
editable: isEntityEditable(value),
};
return columnOption;
});
setColumnOptions(columns);
};
const isEntityEditable = (name: string): boolean => {
if (userContext.apiType === "Cassandra") {
const cassandraKeys = queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
return !cassandraKeys.includes(name);
}
return !(
name === Constants.EntityKeyNames.PartitionKey ||
name === Constants.EntityKeyNames.RowKey ||
name === Constants.EntityKeyNames.Timestamp
);
};
const availableColumnsCheckboxClick = (event: React.FormEvent<HTMLElement>, isChecked: boolean): void => {
setIsAvailableColumnChecked(isChecked);
selectClearAll(isChecked);
};
const selectClearAll = (isChecked: boolean): void => {
const columns: ISelectColumn[] = columnOptions.map((column: ISelectColumn) => {
if (isEntityEditable(column.columnName)) {
column.selected = isChecked;
return { ...column };
}
return { ...column };
});
setColumnOptions(columns);
};
const getParameters = (): string[] => {
const selectedColumns = columnOptions.filter((value: ISelectColumn) => value.selected === true);
const columns: string[] = selectedColumns.map((value: ISelectColumn) => {
const name: string = value.columnName;
return name;
});
return columns;
};
const canSelectAll = (): void => {
const canSelectAllColumn: boolean = columnOptions.some((value: ISelectColumn) => {
return !value.selected;
});
setIsAvailableColumnChecked(!canSelectAllColumn);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<Text>Select the columns that you want to query.</Text>
<div className="column-select-view">
<Checkbox
id="availableCheckbox"
label="Available Columns"
checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick}
/>
{columnOptions.map((column) => {
return (
<Checkbox
label={column.columnName}
onChange={(_event, isChecked: boolean) => handleClick(isChecked, column.columnName)}
key={column.columnName}
checked={column.selected}
disabled={!column.editable}
/>
);
})}
</div>
</div>
</div>
</GenericRightPaneComponent>
);
};

View File

@@ -238,6 +238,52 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -301,6 +347,54 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -495,6 +589,24 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -621,6 +733,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -642,6 +755,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -667,6 +781,20 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -695,6 +823,27 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -726,6 +875,22 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -773,6 +938,32 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],

View File

@@ -8,6 +8,8 @@ import Explorer from "../../Explorer";
import { getErrorMessage } from "../../Tables/Utilities"; import { getErrorMessage } from "../../Tables/Utilities";
import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent"; import { GenericRightPaneComponent, GenericRightPaneProps } from "../GenericRightPaneComponent";
const UPLOAD_FILE_SIZE_LIMIT_KB = 2097152;
export interface UploadItemsPaneProps { export interface UploadItemsPaneProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
@@ -45,6 +47,10 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
setFormError("No files specified"); setFormError("No files specified");
setFormErrorDetail("No files were specified. Please input at least one file."); setFormErrorDetail("No files were specified. Please input at least one file.");
logConsoleError("Could not upload items -- No files were specified. Please input at least one file."); logConsoleError("Could not upload items -- No files were specified. Please input at least one file.");
} else if (_totalFileSizeForFileList(files) > UPLOAD_FILE_SIZE_LIMIT_KB) {
setFormError("Upload file size limit exceeded");
setFormErrorDetail("Total file upload size exceeds the 2 MB file size limit.");
logConsoleError("Could not upload items -- Total file upload size exceeds the 2 MB file size limit.");
} }
const selectedCollection = explorer.findSelectedCollection(); const selectedCollection = explorer.findSelectedCollection();
@@ -73,6 +79,14 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
setFiles(event.target.files); setFiles(event.target.files);
}; };
const _totalFileSizeForFileList = (fileList: FileList): number => {
let totalFileSize = 0;
for (let i = 0; i < fileList?.length; i++) {
totalFileSize += fileList.item(i).size;
}
return totalFileSize;
};
const genericPaneProps: GenericRightPaneProps = { const genericPaneProps: GenericRightPaneProps = {
container: explorer, container: explorer,
formError, formError,

View File

@@ -30,7 +30,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
verticalAlign="start" verticalAlign="start"
> >
<div <div
className="ms-Stack panelInfoErrorContainer css-108" className="ms-Stack panelInfoErrorContainer css-204"
> >
<StyledIconBase <StyledIconBase
className="panelWarningIcon" className="panelWarningIcon"
@@ -317,7 +317,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="panelWarningIcon root-110" className="panelWarningIcon root-206"
data-icon-name="WarningSolid" data-icon-name="WarningSolid"
> >
@@ -333,7 +333,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small" variant="small"
> >
<span <span
className="panelWarningErrorMessage css-111" className="panelWarningErrorMessage css-207"
> >
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources. Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
@@ -358,7 +358,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small" variant="small"
> >
<span <span
className="css-111" className="css-207"
> >
Confirm by typing the collection id Confirm by typing the collection id
</span> </span>
@@ -659,18 +659,18 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField root-113" className="ms-TextField root-209"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-114" className="ms-TextField-fieldGroup fieldGroup-210"
> >
<input <input
aria-invalid={false} aria-invalid={false}
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-115" className="ms-TextField-field field-211"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -693,7 +693,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small" variant="small"
> >
<span <span
className="css-124" className="css-220"
> >
Help us improve Azure Cosmos DB! Help us improve Azure Cosmos DB!
</span> </span>
@@ -703,7 +703,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variant="small" variant="small"
> >
<span <span
className="css-124" className="css-220"
> >
What is the reason why you are deleting this container? What is the reason why you are deleting this container?
</span> </span>
@@ -1006,17 +1006,17 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField ms-TextField--multiline root-113" className="ms-TextField ms-TextField--multiline root-209"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-125" className="ms-TextField-fieldGroup fieldGroup-221"
> >
<textarea <textarea
aria-invalid={false} aria-invalid={false}
className="ms-TextField-field field-126" className="ms-TextField-field field-222"
id="deleteCollectionFeedbackInput" id="deleteCollectionFeedbackInput"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -2708,7 +2708,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
variantClassName="ms-Button--primary" variantClassName="ms-Button--primary"
> >
<button <button
className="ms-Button ms-Button--primary root-128" className="ms-Button ms-Button--primary root-224"
data-is-focusable={true} data-is-focusable={true}
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
@@ -2720,14 +2720,14 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
type="submit" type="submit"
> >
<span <span
className="ms-Button-flexContainer flexContainer-129" className="ms-Button-flexContainer flexContainer-225"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<span <span
className="ms-Button-textContainer textContainer-130" className="ms-Button-textContainer textContainer-226"
> >
<span <span
className="ms-Button-label label-132" className="ms-Button-label label-228"
id="id__6" id="id__6"
key="id__6" key="id__6"
> >

View File

@@ -239,6 +239,52 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
NewVertexPane { NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@@ -302,6 +348,54 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"userTableQuery": [Function], "userTableQuery": [Function],
"visible": [Function], "visible": [Function],
}, },
LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
StringInputPane { StringInputPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -496,6 +590,24 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "browsequeriespane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"loadSavedQuery": [Function],
"queriesGridComponentAdapter": QueriesGridComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"setupQueries": [Function],
"title": [Function],
"visible": [Function],
},
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
@@ -622,6 +734,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"flight": [Function],
"graphStylingPane": GraphStylingPane { "graphStylingPane": GraphStylingPane {
"container": [Circular], "container": [Circular],
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
@@ -643,6 +756,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"visible": [Function], "visible": [Function],
}, },
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
@@ -671,6 +785,20 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"loadQueryPane": LoadQueryPane {
"container": [Circular],
"files": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "loadquerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"onImportLinkKeyPress": [Function],
"selectedFilesTitle": [Function],
"title": [Function],
"visible": [Function],
},
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
@@ -699,6 +827,27 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"querySelectPane": QuerySelectPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsTableQueryLabel": "Available Columns",
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "queryselectpane",
"instructionLabel": "Select the columns that you want to query.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Select Columns",
"visible": [Function],
},
"refreshAllDatabases": [Function], "refreshAllDatabases": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
@@ -731,6 +880,22 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
"saveQueryPane": SaveQueryPane {
"canSaveQueries": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "savequerypane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"queryName": [Function],
"setupQueries": [Function],
"setupSaveQueriesText": "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.",
"submit": [Function],
"title": [Function],
"visible": [Function],
},
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
@@ -778,6 +943,32 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"subscriptionType": [Function],
"tableColumnOptionsPane": TableColumnOptionsPane {
"allSelected": [Function],
"anyColumnSelected": [Function],
"availableColumnsLabel": "Available Columns",
"canMoveDown": [Function],
"canMoveUp": [Function],
"canSelectAll": [Function],
"columnOptions": [Function],
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"handleClick": [Function],
"id": "tablecolumnoptionspane",
"instructionLabel": "Choose the columns and the order in which you want to display them in the table.",
"isExecuting": [Function],
"isTemplateReady": [Function],
"moveDownLabel": "Move Down",
"moveUpLabel": "Move Up",
"noColumnSelectedWarning": "At least one column should be selected.",
"selectedColumnOption": null,
"title": [Function],
"titleLabel": "Column Options",
"visible": [Function],
},
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
@@ -808,7 +999,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
verticalAlign="start" verticalAlign="start"
> >
<div <div
className="ms-Stack panelInfoErrorContainer css-140" className="ms-Stack panelInfoErrorContainer css-204"
> >
<StyledIconBase <StyledIconBase
className="panelWarningIcon" className="panelWarningIcon"
@@ -1095,7 +1286,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
> >
<i <i
aria-hidden={true} aria-hidden={true}
className="panelWarningIcon root-142" className="panelWarningIcon root-206"
data-icon-name="WarningSolid" data-icon-name="WarningSolid"
> >
@@ -1111,7 +1302,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small" variant="small"
> >
<span <span
className="panelWarningErrorMessage css-143" className="panelWarningErrorMessage css-207"
> >
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources. Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
@@ -1136,7 +1327,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small" variant="small"
> >
<span <span
className="css-143" className="css-207"
> >
Confirm by typing the database id Confirm by typing the database id
</span> </span>
@@ -1437,18 +1628,18 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField root-145" className="ms-TextField root-209"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-146" className="ms-TextField-fieldGroup fieldGroup-210"
> >
<input <input
aria-invalid={false} aria-invalid={false}
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-147" className="ms-TextField-field field-211"
id="confirmDatabaseId" id="confirmDatabaseId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -1471,7 +1662,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small" variant="small"
> >
<span <span
className="css-164" className="css-228"
> >
Help us improve Azure Cosmos DB! Help us improve Azure Cosmos DB!
</span> </span>
@@ -1481,7 +1672,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small" variant="small"
> >
<span <span
className="css-164" className="css-228"
> >
What is the reason why you are deleting this database? What is the reason why you are deleting this database?
</span> </span>
@@ -1784,17 +1975,17 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
validateOnLoad={true} validateOnLoad={true}
> >
<div <div
className="ms-TextField ms-TextField--multiline root-145" className="ms-TextField ms-TextField--multiline root-209"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-165" className="ms-TextField-fieldGroup fieldGroup-229"
> >
<textarea <textarea
aria-invalid={false} aria-invalid={false}
className="ms-TextField-field field-166" className="ms-TextField-field field-230"
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -3486,7 +3677,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variantClassName="ms-Button--primary" variantClassName="ms-Button--primary"
> >
<button <button
className="ms-Button ms-Button--primary root-156" className="ms-Button ms-Button--primary root-220"
data-is-focusable={true} data-is-focusable={true}
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
@@ -3498,14 +3689,14 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit" type="submit"
> >
<span <span
className="ms-Button-flexContainer flexContainer-157" className="ms-Button-flexContainer flexContainer-221"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<span <span
className="ms-Button-textContainer textContainer-158" className="ms-Button-textContainer textContainer-222"
> >
<span <span
className="ms-Button-label label-160" className="ms-Button-label label-224"
id="id__3" id="id__3"
key="id__3" key="id__3"
> >

View File

@@ -50,6 +50,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
this.subscriptions = []; this.subscriptions = [];
} }
public shouldComponentUpdate() {
return this.container.tabsManager.openedTabs.length === 0;
}
public componentWillUnmount() { public componentWillUnmount() {
while (this.subscriptions.length) { while (this.subscriptions.length) {
this.subscriptions.pop().dispose(); this.subscriptions.pop().dispose();
@@ -58,6 +62,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() { public componentDidMount() {
this.subscriptions.push( this.subscriptions.push(
this.container.tabsManager.openedTabs.subscribe(() => this.setState({})),
this.container.selectedNode.subscribe(() => this.setState({})), this.container.selectedNode.subscribe(() => this.setState({})),
this.container.isNotebookEnabled.subscribe(() => this.setState({})) this.container.isNotebookEnabled.subscribe(() => this.setState({}))
); );
@@ -75,13 +80,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
const tipsItems = this.createTipsItems(); const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent; const onClearRecent = this.clearMostRecent;
const formContainer = (jsx: JSX.Element) => ( return (
<div className="connectExplorerContainer">
<form className="connectExplorerFormContainer">{jsx}</form>
</div>
);
return formContainer(
<div className="splashScreenContainer"> <div className="splashScreenContainer">
<div className="splashScreen"> <div className="splashScreen">
<div className="title"> <div className="title">
@@ -253,7 +252,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: OpenQueryIcon, iconSrc: OpenQueryIcon,
title: "Open Query", title: "Open Query",
description: null, description: null,
onClick: () => this.container.openBrowseQueriesPanel(), onClick: () => this.container.browseQueriesPane.open(),
}); });
if (!this.container.isPreferredApiCassandra()) { if (!this.container.isPreferredApiCassandra()) {

View File

@@ -1,8 +1,12 @@
import _ from "underscore";
import Q from "q"; import Q from "q";
import Explorer from "../../Explorer";
import * as Entities from "../Entities";
import * as DataTableUtilities from "./DataTableUtilities"; import * as DataTableUtilities from "./DataTableUtilities";
import * as DataTableOperations from "./DataTableOperations";
import TableEntityListViewModel from "./TableEntityListViewModel"; import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Entities from "../Entities";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as TableColumnOptionsPane from "../../Panes/Tables/TableColumnOptionsPane";
import Explorer from "../../Explorer";
export default class TableCommands { export default class TableCommands {
// Command Ids // Command Ids
@@ -88,6 +92,64 @@ export default class TableCommands {
return null; return null;
} }
public customizeColumnsCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
var table: DataTables.DataTable = viewModel.table;
var displayedColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
var columnsCount: number = displayedColumnNames.length;
var currentOrder: number[] = DataTableOperations.getInitialOrder(columnsCount);
//Debug.assert(!!table && !!currentOrder && displayedColumnNames.length === currentOrder.length);
var currentSettings: boolean[];
try {
currentSettings = currentOrder.map((value: number, index: number) => {
return table.column(index).visible();
});
} catch (err) {
// Error
}
let parameters: TableColumnOptionsPane.IColumnSetting = <TableColumnOptionsPane.IColumnSetting>{
columnNames: displayedColumnNames,
order: currentOrder,
visible: currentSettings,
};
this._container.tableColumnOptionsPane.tableViewModel = viewModel;
this._container.tableColumnOptionsPane.parameters = parameters;
this._container.tableColumnOptionsPane.open();
return null;
}
public reorderColumnsBasedOnSelectedEntities(viewModel: TableEntityListViewModel): Q.Promise<boolean> {
var selected = viewModel.selected();
if (!selected || !selected.length) {
return null;
}
var table = viewModel.table;
var currentColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
var headersCount: number = currentColumnNames.length;
var headersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
selected,
viewModel.queryTablesTab.container.isPreferredApiCassandra()
);
// An array with elements representing indexes of selected entities' header union out of initial headers.
var orderOfLeftHeaders: number[] = headersUnion.map((item: string) => currentColumnNames.indexOf(item));
// An array with elements representing initial order of the table.
var initialOrder: number[] = DataTableOperations.getInitialOrder(headersCount);
// An array with elements representing indexes of headers not present in selected entities' header union.
var orderOfRightHeaders: number[] = _.difference(initialOrder, orderOfLeftHeaders);
// This will be the target order, with headers in selected entities on the left while others on the right, both in the initial order, respectively.
var targetOrder: number[] = orderOfLeftHeaders.concat(orderOfRightHeaders);
return DataTableOperations.reorderColumns(table, targetOrder);
}
public resetColumns(viewModel: TableEntityListViewModel): void { public resetColumns(viewModel: TableEntityListViewModel): void {
viewModel.reloadTable(); viewModel.reloadTable();
} }

View File

@@ -1,12 +1,13 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import { KeyCodes } from "../../../Common/Constants";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import QueryBuilderViewModel from "./QueryBuilderViewModel"; import QueryBuilderViewModel from "./QueryBuilderViewModel";
import QueryClauseViewModel from "./QueryClauseViewModel"; import QueryClauseViewModel from "./QueryClauseViewModel";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
import { KeyCodes } from "../../../Common/Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
export default class QueryViewModel { export default class QueryViewModel {
public topValueLimitMessage: string = "Please input a number between 0 and 1000."; public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
@@ -197,7 +198,8 @@ export default class QueryViewModel {
}; };
public selectQueryOptions(): Promise<any> { public selectQueryOptions(): Promise<any> {
this.queryTablesTab.container.openTableSelectQueryPanel(this); this.queryTablesTab.container.querySelectPane.queryViewModel = this;
this.queryTablesTab.container.querySelectPane.open();
return null; return null;
} }

View File

@@ -0,0 +1,85 @@
<div
class="tab-pane flexContainer"
data-bind="
attr:{
id: tabId
},
visible: isActive"
role="tabpanel"
>
<div class="warningErrorContainer scaleWarningContainer" data-bind="visible: shouldShowStatusBar">
<div>
<div class="warningErrorContent" data-bind="visible: shouldShowNotificationStatusPrompt">
<span><img src="/info_color.svg" alt="Info" /></span>
<span class="warningErrorDetailsLinkContainer" data-bind="html: notificationStatusInfo"></span>
</div>
<div class="warningErrorContent" data-bind="visible: !shouldShowNotificationStatusPrompt()">
<span><img src="/warning.svg" alt="Warning" /></span>
<span class="warningErrorDetailsLinkContainer" data-bind="html: warningMessage"></span>
</div>
</div>
</div>
<div class="tabForm scaleSettingScrollable">
<div class="scaleDivison" aria-label="Scale" aria-controls="scaleRegion">
<span class="scaleSettingTitle">Scale</span>
</div>
<div class="freeTierInfoBanner" data-bind="visible: isFreeTierAccount">
<span class="freeTierInfoIcon"><img src="/info_color.svg" alt="Info" /></span>
<span class="freeTierInfoMessage"
>With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. To keep your
account free, keep the total RU/s across all resources in the account to 400 RU/s.
<a
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank"
>
Learn more.</a
>
</span>
</div>
<div class="ssTextAllignment" id="scaleRegion">
<throughput-input-autopilot-v3
params="{
testId: testId,
class: 'scaleForm dirty',
value: throughput,
minimum: minRUs,
maximum: maxRUThroughputInputLimit,
canExceedMaximumValue: canThroughputExceedMaximumValue,
step: throughputIncreaseFactor,
label: throughputTitle,
ariaLabel: throughputAriaLabel,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
throughputAutoPilotRadioId: throughputAutoPilotRadioId,
throughputProvisionedRadioId: throughputProvisionedRadioId,
throughputModeRadioName: throughputModeRadioName,
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
overrideWithAutoPilotSettings: overrideWithAutoPilotSettings,
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings,
freeTierExceedThroughputWarning: freeTierExceedThroughputWarning
}"
>
</throughput-input-autopilot-v3>
<div class="estimatedCost" data-bind="visible: costsVisible">
<p data-bind="visible: minRUAnotationVisible">
<span>Learn more about minimum throughput </span>
<a href="https://docs.microsoft.com/azure/cosmos-db/set-throughput" target="_blank">here.</a>
</p>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade -->
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request"
>Contact support</a
>
for more than <span data-bind="text: maxRUsText"></span> RU/s
</p>
<p data-bind="visible: shouldDisplayPortalUsePrompt">
Use Data Explorer from Azure Portal to request more than <span data-bind="text: maxRUsText"></span> RU/s
</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,489 @@
import * as ko from "knockout";
import Q from "q";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { updateOffer } from "../../Common/dataAccess/updateOffer";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as SharedConstants from "../../Shared/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import template from "./DatabaseSettingsTab.html";
import TabsBase from "./TabsBase";
const updateThroughputBeyondLimitWarningMessage: string = `
You are about to request an increase in throughput beyond the pre-allocated capacity.
The service will scale out and increase throughput for the selected database.
This operation will take 1-3 business days to complete. You can track the status of this request in Notifications.`;
const updateThroughputDelayedApplyWarningMessage: string = `
You are about to request an increase in throughput beyond the pre-allocated capacity.
This operation will take some time to complete.`;
const currentThroughput: (isAutoscale: boolean, throughput: number) => string = (isAutoscale, throughput) =>
isAutoscale
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
: `Current manual throughput: ${throughput} RU/s`;
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
`A request to increase the throughput is currently in progress.
This operation will take some time to complete.<br />
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`;
const throughputApplyLongDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
`A request to increase the throughput is currently in progress.
This operation will take 1-3 business days to complete. View the latest status in Notifications.<br />
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`;
export default class DatabaseSettingsTab extends TabsBase implements ViewModels.WaitsForTemplate {
public static readonly component = { name: "database-settings-tab", template };
// editables
public isAutoPilotSelected: ViewModels.Editable<boolean>;
public throughput: ViewModels.Editable<number>;
public autoPilotThroughput: ViewModels.Editable<number>;
public throughputIncreaseFactor: number = Constants.ClientDefaults.databaseThroughputIncreaseFactor;
public saveSettingsButton: ViewModels.Button;
public discardSettingsChangesButton: ViewModels.Button;
public canRequestSupport: ko.PureComputed<boolean>;
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
public costsVisible: ko.Computed<boolean>;
public displayedError: ko.Observable<string>;
public isFreeTierAccount: ko.Computed<boolean>;
public isTemplateReady: ko.Observable<boolean>;
public minRUAnotationVisible: ko.Computed<boolean>;
public minRUs: ko.Observable<number>;
public maxRUs: ko.Observable<number>;
public maxRUsText: ko.PureComputed<string>;
public maxRUThroughputInputLimit: ko.Computed<number>;
public notificationStatusInfo: ko.Observable<string>;
public pendingNotification: ko.Observable<DataModels.Notification>;
public requestUnitsUsageCost: ko.PureComputed<string>;
public autoscaleCost: ko.PureComputed<string>;
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
public shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
public shouldShowStatusBar: ko.Computed<boolean>;
public throughputTitle: ko.PureComputed<string>;
public throughputAriaLabel: ko.PureComputed<string>;
public autoPilotUsageCost: ko.PureComputed<string>;
public warningMessage: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public overrideWithAutoPilotSettings: ko.Computed<boolean>;
public overrideWithProvisionedThroughputSettings: ko.Computed<boolean>;
public testId: string;
public throughputAutoPilotRadioId: string;
public throughputProvisionedRadioId: string;
public throughputModeRadioName: string;
public freeTierExceedThroughputWarning: ko.Computed<string>;
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
private _offerReplacePending: ko.Observable<boolean>;
private container: Explorer;
constructor(options: ViewModels.TabOptions) {
super(options);
this.container = options.node && (options.node as ViewModels.Database).container;
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
// html element ids
this.testId = `scaleSettingThroughputValue${this.tabId}`;
this.throughputAutoPilotRadioId = `editContainerThroughput-autoPilotRadio${this.tabId}`;
this.throughputProvisionedRadioId = `editContainerThroughput-manualRadio${this.tabId}`;
this.throughputModeRadioName = `throughputModeRadio${this.tabId}`;
this.throughput = editable.observable<number>();
this._wasAutopilotOriginallySet = ko.observable(false);
this.isAutoPilotSelected = editable.observable(false);
this.autoPilotThroughput = editable.observable<number>();
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput) {
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
this._wasAutopilotOriginallySet(true);
this.isAutoPilotSelected(true);
this.autoPilotThroughput(autoscaleMaxThroughput);
}
}
this._hasProvisioningTypeChanged = ko.pureComputed<boolean>(() => {
if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) {
return true;
}
return false;
});
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this.autoPilotThroughput();
if (!autoPilot) {
return "";
}
return PricingUtils.getAutoPilotV3SpendHtml(autoPilot, true /* isDatabaseThroughput */);
});
this.requestUnitsUsageCost = ko.pureComputed(() => {
const account = userContext.databaseAccount;
if (!account) {
return "";
}
const regions =
(account &&
account.properties &&
account.properties.readLocations &&
account.properties.readLocations.length) ||
1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
let estimatedSpend: string;
if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(),
userContext.portalEnv,
regions,
multimaster
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.autoPilotThroughput(),
userContext.portalEnv,
regions,
multimaster
);
}
return estimatedSpend;
});
this.costsVisible = ko.computed(() => {
return configContext.platform !== Platform.Emulator;
});
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(() => configContext.platform === Platform.Hosted);
this.canThroughputExceedMaximumValue = ko.pureComputed<boolean>(
() => configContext.platform === Platform.Portal && !this.container.isRunningOnNationalCloud()
);
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform === Platform.Emulator ||
configContext.platform === Platform.Hosted ||
this.canThroughputExceedMaximumValue()
) {
return false;
}
return true;
});
this.overrideWithAutoPilotSettings = ko.pureComputed(() => {
return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet();
});
this.overrideWithProvisionedThroughputSettings = ko.pureComputed(() => {
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
});
this.minRUs = ko.observable<number>(
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
);
this.minRUAnotationVisible = ko.computed<boolean>(() => {
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
});
this.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
if (configContext.platform === Platform.Hosted) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
return this.maxRUs();
});
this.maxRUsText = ko.pureComputed(() => {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString();
});
this.throughputTitle = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
return `Throughput (${this.minRUs().toLocaleString()} - unlimited RU/s)`;
});
this.throughputAriaLabel = ko.pureComputed<string>(() => {
return this.throughputTitle() + this.requestUnitsUsageCost();
});
this.pendingNotification = ko.observable<DataModels.Notification>();
this._offerReplacePending = ko.observable<boolean>(!!this.database.offer()?.offerReplacePending);
this.notificationStatusInfo = ko.observable<string>("");
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
this.warningMessage = ko.computed<string>(() => {
if (this.overrideWithProvisionedThroughputSettings()) {
return AutoPilotUtils.manualToAutoscaleDisclaimer;
}
const offer = this.database.offer();
if (offer?.offerReplacePending) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
}
if (
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.canThroughputExceedMaximumValue()
) {
return updateThroughputBeyondLimitWarningMessage;
}
if (this.throughput() > this.maxRUs()) {
return updateThroughputDelayedApplyWarningMessage;
}
if (this.pendingNotification()) {
const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s");
const throughput: number = matches.length > 1 && Number(matches[1]);
if (throughput) {
return throughputApplyLongDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
}
}
return "";
});
this.warningMessage.subscribe((warning: string) => {
if (warning.length > 0) {
this.notificationStatusInfo("");
}
});
this.shouldShowStatusBar = ko.computed<boolean>(
() => this.shouldShowNotificationStatusPrompt() || (this.warningMessage && this.warningMessage().length > 0)
);
this.displayedError = ko.observable<string>("");
this._setBaseline();
this.saveSettingsButton = {
enabled: ko.computed<boolean>(() => {
if (this._hasProvisioningTypeChanged()) {
return true;
}
if (this._offerReplacePending && this._offerReplacePending()) {
return false;
}
const isAutoPilot = this.isAutoPilotSelected();
const isManual = !this.isAutoPilotSelected();
if (isAutoPilot) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(this.autoPilotThroughput())) {
return false;
}
if (this.isAutoPilotSelected.editableIsDirty()) {
return true;
}
if (this.autoPilotThroughput.editableIsDirty()) {
return true;
}
}
if (isManual) {
if (!this.throughput()) {
return false;
}
if (this.throughput() < this.minRUs()) {
return false;
}
if (
!this.canThroughputExceedMaximumValue() &&
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
) {
return false;
}
if (this.throughput.editableIsDirty()) {
return true;
}
if (this.isAutoPilotSelected.editableIsDirty()) {
return true;
}
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.discardSettingsChangesButton = {
enabled: ko.computed<boolean>(() => {
if (this.throughput.editableIsDirty()) {
return true;
}
if (this.isAutoPilotSelected.editableIsDirty()) {
return true;
}
if (this.autoPilotThroughput.editableIsDirty()) {
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.isTemplateReady = ko.observable<boolean>(false);
this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = userContext.databaseAccount;
return databaseAccount?.properties?.enableFreeTier;
});
this.freeTierExceedThroughputWarning = ko.computed<string>(() =>
this.isFreeTierAccount()
? "Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale."
: ""
);
this._buildCommandBarOptions();
}
public onSaveClick = async (): Promise<any> => {
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateSettings, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
});
try {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.database.id(),
currentOffer: this.database.offer(),
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput(),
};
if (this._hasProvisioningTypeChanged()) {
if (this.isAutoPilotSelected()) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.database.offer(updatedOffer);
this.database.offer.valueHasMutated();
this._setBaseline();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
} catch (error) {
this.isExecutionError(true);
console.error(error);
const errorMessage = getErrorMessage(error);
this.displayedError(errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateSettings,
{
databaseName: this.database && this.database.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
} finally {
this.isExecuting(false);
}
};
public onRevertClick = (): Q.Promise<any> => {
this.throughput.setBaseline(this.throughput.getEditableOriginalValue());
this.isAutoPilotSelected.setBaseline(this.isAutoPilotSelected.getEditableOriginalValue());
this.autoPilotThroughput.setBaseline(this.autoPilotThroughput.getEditableOriginalValue());
return Q();
};
public async onActivate(): Promise<void> {
super.onActivate();
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
await this.database.loadOffer();
}
private _setBaseline() {
const offer = this.database && this.database.offer && this.database.offer();
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
this.throughput.setBaseline(offer.manualThroughput);
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = "Save";
if (this.saveSettingsButton.visible()) {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveSettingsButton.enabled(),
});
}
if (this.discardSettingsChangesButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardSettingsChangesButton.enabled(),
});
}
return buttons;
}
private _buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([
this.saveSettingsButton.visible,
this.saveSettingsButton.enabled,
this.discardSettingsChangesButton.visible,
this.discardSettingsChangesButton.enabled,
])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
}

View File

@@ -6,11 +6,11 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { isInvalidParentFrameOrigin, isReadyMessage } from "../../Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import template from "./MongoShellTab.html"; import template from "./MongoShellTab.html";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
export default class MongoShellTab extends TabsBase { export default class MongoShellTab extends TabsBase {
@@ -85,7 +85,10 @@ export default class MongoShellTab extends TabsBase {
} }
private handleReadyMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) { private handleReadyMessage(event: MessageEvent, shellIframe: HTMLIFrameElement) {
if (!isReadyMessage(event)) { if (typeof event.data["kind"] !== "string") {
return;
}
if (event.data.kind !== "ready") {
return; return;
} }

View File

@@ -1,36 +1,39 @@
import { stringifyNotebook, toJS } from "@nteract/commutable";
import * as ko from "knockout";
import * as Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; import * as Q from "q";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import * as ko from "knockout";
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
import PasteIcon from "../../../images/notebook/Notebook-paste.svg";
import RestartIcon from "../../../images/notebook/Notebook-restart.svg";
import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
import RunIcon from "../../../images/notebook/Notebook-run.svg";
import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import { Areas, ArmApiVersions } from "../../Common/Constants";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { trackEvent } from "../../Shared/appInsights"; import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import TabsBase from "./TabsBase";
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import PasteIcon from "../../../images/notebook/Notebook-paste.svg";
import RunIcon from "../../../images/notebook/Notebook-run.svg";
import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
import RestartIcon from "../../../images/notebook/Notebook-restart.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { Areas, ArmApiVersions } from "../../Common/Constants";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { configContext } from "../../ConfigContext";
import Explorer from "../Explorer";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { toJS, stringifyNotebook } from "@nteract/commutable";
import { appInsights } from "../../Shared/appInsights";
import { userContext } from "../../UserContext";
import template from "./NotebookV2Tab.html"; import template from "./NotebookV2Tab.html";
import TabsBase from "./TabsBase";
export interface NotebookTabOptions extends ViewModels.TabOptions { export interface NotebookTabOptions extends ViewModels.TabOptions {
account: DataModels.DatabaseAccount; account: DataModels.DatabaseAccount;
@@ -425,7 +428,7 @@ export default class NotebookTabV2 extends TabsBase {
return; return;
} }
trackEvent( appInsights.trackEvent(
{ name: "SparkPoolSelected" }, { name: "SparkPoolSelected" },
{ {
subscriptionId: userContext.subscriptionId, subscriptionId: userContext.subscriptionId,

View File

@@ -1,22 +1,23 @@
import * as ko from "knockout"; import * as ko from "knockout";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { HashMap } from "../../Common/HashMap";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import template from "./QueryTab.html";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
import { HashMap } from "../../Common/HashMap";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import * as QueryUtils from "../../Utils/QueryUtils";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
import template from "./QueryTab.html";
enum ToggleState { enum ToggleState {
Result, Result,
@@ -184,12 +185,16 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
await this._executeQueryDocumentsPage(0); await this._executeQueryDocumentsPage(0);
}; };
public onLoadQueryClick = (): void => {
this.collection && this.collection.container && this.collection.container.loadQueryPane.open();
};
public onSaveQueryClick = (): void => { public onSaveQueryClick = (): void => {
this.collection && this.collection.container && this.collection.container.openSaveQueryPanel(); this.collection && this.collection.container && this.collection.container.saveQueryPane.open();
}; };
public onSavedQueriesClick = (): void => { public onSavedQueriesClick = (): void => {
this.collection && this.collection.container && this.collection.container.openBrowseQueriesPanel(); this.collection && this.collection.container && this.collection.container.browseQueriesPane.open();
}; };
public async onFetchNextPageClick(): Promise<void> { public async onFetchNextPageClick(): Promise<void> {

View File

@@ -1,22 +1,23 @@
import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants"; import * as ko from "knockout";
import { readCollections } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { IJunoResponse, JunoClient } from "../../Juno/JunoClient"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
import Collection from "./Collection"; import Collection from "./Collection";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer";
import { readCollections } from "../../Common/dataAccess/readCollections";
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
import { userContext } from "../../UserContext";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class Database implements ViewModels.Database { export default class Database implements ViewModels.Database {
public nodeKind: string; public nodeKind: string;
@@ -57,13 +58,18 @@ export default class Database implements ViewModels.Database {
}); });
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification(); const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2; const useDatabaseSettingsTabV1 = userContext.features.enableDatabaseSettingsTabV1;
const tabKind: ViewModels.CollectionTabKind = useDatabaseSettingsTabV1
? ViewModels.CollectionTabKind.DatabaseSettings
: ViewModels.CollectionTabKind.DatabaseSettingsV2;
const matchingTabs = this.container.tabsManager.getTabs(tabKind, (tab) => tab.node?.id() === this.id()); const matchingTabs = this.container.tabsManager.getTabs(tabKind, (tab) => tab.node?.id() === this.id());
let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2; let settingsTab: DatabaseSettingsTab | DatabaseSettingsTabV2 = useDatabaseSettingsTabV1
? (matchingTabs?.[0] as DatabaseSettingsTab)
: (matchingTabs?.[0] as DatabaseSettingsTabV2);
if (!settingsTab) { if (!settingsTab) {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.id(), databaseName: this.id(),
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale", tabTitle: "Scale",
}); });
@@ -71,7 +77,9 @@ export default class Database implements ViewModels.Database {
(data: any) => { (data: any) => {
const pendingNotification: DataModels.Notification = data?.[0]; const pendingNotification: DataModels.Notification = data?.[0];
const tabOptions: ViewModels.TabOptions = { const tabOptions: ViewModels.TabOptions = {
tabKind, tabKind: useDatabaseSettingsTabV1
? ViewModels.CollectionTabKind.DatabaseSettings
: ViewModels.CollectionTabKind.DatabaseSettingsV2,
title: "Scale", title: "Scale",
tabPath: "", tabPath: "",
node: this, node: this,
@@ -82,7 +90,9 @@ export default class Database implements ViewModels.Database {
onLoadStartKey: startKey, onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons, onUpdateTabsButtons: this.container.onUpdateTabsButtons,
}; };
settingsTab = new DatabaseSettingsTabV2(tabOptions); settingsTab = useDatabaseSettingsTabV1
? new DatabaseSettingsTab(tabOptions)
: new DatabaseSettingsTabV2(tabOptions);
settingsTab.pendingNotification(pendingNotification); settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateNewTab(settingsTab); this.container.tabsManager.activateNewTab(settingsTab);
}, },

View File

@@ -1,8 +1,8 @@
{ {
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance.", "DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance.",
"DedicatedGateway": "Dedicated Gateway", "DedicatedGateway": "Dedicated Gateway",
"Provisioned": "Provisioned", "Enable": "Enable",
"Deprovisioned": "Deprovisioned", "Disable": "Disable",
"LearnAboutDedicatedGateway": "Learn more about dedicated gateway.", "LearnAboutDedicatedGateway": "Learn more about dedicated gateway.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.", "DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.",
"DedicatedGatewayPricing": "Learn more about dedicated gateway pricing.", "DedicatedGatewayPricing": "Learn more about dedicated gateway pricing.",

View File

@@ -53,7 +53,6 @@ import "./Explorer/Tabs/QueryTab.less";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useSidePanel } from "./hooks/useSidePanel"; import { useSidePanel } from "./hooks/useSidePanel";
import { useTabs } from "./hooks/useTabs";
import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import "./Libs/jquery"; import "./Libs/jquery";
import "./Shared/appInsights"; import "./Shared/appInsights";
@@ -79,7 +78,6 @@ const App: React.FunctionComponent = () => {
}; };
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel(); const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
const { tabs, tabsManager } = useTabs();
const explorerParams: ExplorerParams = { const explorerParams: ExplorerParams = {
setIsNotificationConsoleExpanded, setIsNotificationConsoleExpanded,
@@ -89,9 +87,7 @@ const App: React.FunctionComponent = () => {
closeSidePanel, closeSidePanel,
openDialog, openDialog,
closeDialog, closeDialog,
tabsManager,
}; };
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams); const explorer = useKnockoutExplorer(config?.platform, explorerParams);
@@ -204,7 +200,11 @@ const App: React.FunctionComponent = () => {
{/* Splitter - End */} {/* Splitter - End */}
</div> </div>
{/* Collections Tree - End */} {/* Collections Tree - End */}
{tabs.length === 0 && <SplashScreen explorer={explorer} />} <div className="connectExplorerContainer" data-bind="visible: tabsManager.openedTabs().length === 0">
<form className="connectExplorerFormContainer">
<SplashScreen explorer={explorer} />
</form>
</div>
<div className="tabsManagerContainer" data-bind='component: { name: "tabs-manager", params: tabsManager }' /> <div className="tabsManagerContainer" data-bind='component: { name: "tabs-manager", params: tabsManager }' />
</div> </div>
{/* Collections Tree and Tabs - End */} {/* Collections Tree and Tabs - End */}
@@ -236,7 +236,12 @@ const App: React.FunctionComponent = () => {
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' /> <div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
<div data-bind='component: { name: "table-add-entity-pane", params: { data: addTableEntityPane} }' /> <div data-bind='component: { name: "table-add-entity-pane", params: { data: addTableEntityPane} }' />
<div data-bind='component: { name: "table-edit-entity-pane", params: { data: editTableEntityPane} }' /> <div data-bind='component: { name: "table-edit-entity-pane", params: { data: editTableEntityPane} }' />
<div data-bind='component: { name: "table-column-options-pane", params: { data: tableColumnOptionsPane} }' />
<div data-bind='component: { name: "table-query-select-pane", params: { data: querySelectPane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' /> <div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
<div data-bind='component: { name: "load-query-pane", params: { data: loadQueryPane} }' />
<div data-bind='component: { name: "save-query-pane", params: { data: saveQueryPane} }' />
<div data-bind='component: { name: "browse-queries-pane", params: { data: browseQueriesPane} }' />
<div data-bind='component: { name: "string-input-pane", params: { data: stringInputPane} }' /> <div data-bind='component: { name: "string-input-pane", params: { data: stringInputPane} }' />
<div data-bind='component: { name: "setup-notebooks-pane", params: { data: setupNotebooksPane} }' /> <div data-bind='component: { name: "setup-notebooks-pane", params: { data: setupNotebooksPane} }' />
<KOCommentIfStart if="isGitHubPaneEnabled" /> <KOCommentIfStart if="isGitHubPaneEnabled" />

View File

@@ -2,6 +2,7 @@ export type Features = {
readonly canExceedMaximumValue: boolean; readonly canExceedMaximumValue: boolean;
readonly cosmosdb: boolean; readonly cosmosdb: boolean;
readonly enableChangeFeedPolicy: boolean; readonly enableChangeFeedPolicy: boolean;
readonly enableDatabaseSettingsTabV1: boolean;
readonly enableFixedCollectionWithSharedThroughput: boolean; readonly enableFixedCollectionWithSharedThroughput: boolean;
readonly enableKOPanel: boolean; readonly enableKOPanel: boolean;
readonly enableNotebooks: boolean; readonly enableNotebooks: boolean;
@@ -17,14 +18,12 @@ export type Features = {
readonly notebookBasePath?: string; readonly notebookBasePath?: string;
readonly notebookServerToken?: string; readonly notebookServerToken?: string;
readonly notebookServerUrl?: string; readonly notebookServerUrl?: string;
readonly sandboxNotebookOutputs: boolean;
readonly selfServeType?: string; readonly selfServeType?: string;
readonly pr?: string;
readonly showMinRUSurvey: boolean; readonly showMinRUSurvey: boolean;
readonly ttl90Days: boolean; readonly ttl90Days: boolean;
}; };
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features { export function extractFeatures(given = new URLSearchParams()): Features {
const downcased = new URLSearchParams(); const downcased = new URLSearchParams();
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value); const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
const get = (key: string) => downcased.get("feature." + key) ?? undefined; const get = (key: string) => downcased.get("feature." + key) ?? undefined;
@@ -41,6 +40,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
canExceedMaximumValue: "true" === get("canexceedmaximumvalue"), canExceedMaximumValue: "true" === get("canexceedmaximumvalue"),
cosmosdb: "true" === get("cosmosdb"), cosmosdb: "true" === get("cosmosdb"),
enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"), enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"),
enableDatabaseSettingsTabV1: "true" === get("enabledbsettingsv1"),
enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"), enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"),
enableKOPanel: "true" === get("enablekopanel"), enableKOPanel: "true" === get("enablekopanel"),
enableNotebooks: "true" === get("enablenotebooks"), enableNotebooks: "true" === get("enablenotebooks"),
@@ -56,9 +56,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
notebookBasePath: get("notebookbasepath"), notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"), notebookServerToken: get("notebookservertoken"),
notebookServerUrl: get("notebookserverurl"), notebookServerUrl: get("notebookserverurl"),
sandboxNotebookOutputs: "true" === get("sandboxnotebookoutputs"),
selfServeType: get("selfservetype"), selfServeType: get("selfservetype"),
pr: get("pr"),
showMinRUSurvey: "true" === get("showminrusurvey"), showMinRUSurvey: "true" === get("showminrusurvey"),
ttl90Days: "true" === get("ttl90days"), ttl90Days: "true" === get("ttl90days"),
}; };

View File

@@ -23,11 +23,9 @@ const loadTranslationFile = async (className: string): Promise<void> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let translations: any; let translations: any;
try { try {
translations = await import( translations = await import(`../Localization/${language}/${fileName}`);
/* webpackChunkName: "Localization-[request]" */ `../Localization/${language}/${fileName}`
);
} catch (e) { } catch (e) {
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`); translations = await import(`../Localization/en/${fileName}`);
} }
i18n.addResourceBundle(language, className, translations.default, true); i18n.addResourceBundle(language, className, translations.default, true);
}; };
@@ -41,15 +39,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
switch (selfServeType) { switch (selfServeType) {
case SelfServeType.example: { case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample"); const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
const selfServeExample = new SelfServeExample.default(); await loadTranslations(SelfServeExample.default.name);
await loadTranslations(selfServeExample.constructor.name); return new SelfServeExample.default().toSelfServeDescriptor();
return selfServeExample.toSelfServeDescriptor();
} }
case SelfServeType.sqlx: { case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX"); const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
const sqlX = new SqlX.default(); await loadTranslations(SqlX.default.name);
await loadTranslations(sqlX.constructor.name); return new SqlX.default().toSelfServeDescriptor();
return sqlX.toSelfServeDescriptor();
} }
default: default:
return undefined; return undefined;
@@ -111,16 +107,6 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
subscriptionId: inputs.subscriptionId, subscriptionId: inputs.subscriptionId,
}); });
if (i18n.isInitialized) {
await displaySelfServeComponent(selfServeType);
} else {
i18n.on("initialized", async () => {
await displaySelfServeComponent(selfServeType);
});
}
};
const displaySelfServeComponent = async (selfServeType: SelfServeType): Promise<void> => {
const descriptor = await getDescriptor(selfServeType); const descriptor = await getDescriptor(selfServeType);
ReactDOM.render(renderComponent(descriptor), document.getElementById("selfServeContent")); ReactDOM.render(renderComponent(descriptor), document.getElementById("selfServeContent"));
}; };

View File

@@ -1,24 +1,69 @@
import { sendMessage } from "../Common/MessageHandler";
import { configContext } from "../ConfigContext";
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts"; import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
import { appInsights } from "../Shared/appInsights";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { trace, traceCancel, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../UserContext";
import { SelfServeTelemetryMessage } from "./SelfServeTypes"; import { SelfServeTelemetryMessage } from "./SelfServeTypes";
export const selfServeTrace = (data: SelfServeTelemetryMessage): void => { const action = Action.SelfServe;
trace(Action.SelfServe, ActionModifiers.Mark, data, SelfServeMessageTypes.TelemetryInfo);
export const trace = (data: SelfServeTelemetryMessage): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Mark, data);
appInsights.trackEvent({ name: Action[action] }, decorateData(data, ActionModifiers.Mark));
}; };
export const selfServeTraceStart = (data: SelfServeTelemetryMessage): number => { export const traceStart = (data: SelfServeTelemetryMessage): number => {
return traceStart(Action.SelfServe, data, SelfServeMessageTypes.TelemetryInfo); const timestamp: number = Date.now();
sendSelfServeTelemetryMessage(ActionModifiers.Start, data);
appInsights.startTrackEvent(Action[action]);
return timestamp;
}; };
export const selfServeTraceSuccess = (data: SelfServeTelemetryMessage, timestamp?: number): void => { export const traceSuccess = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
traceSuccess(Action.SelfServe, data, timestamp, SelfServeMessageTypes.TelemetryInfo); sendSelfServeTelemetryMessage(ActionModifiers.Success, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Success));
}; };
export const selfServeTraceFailure = (data: SelfServeTelemetryMessage, timestamp?: number): void => { export const traceFailure = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
traceFailure(Action.SelfServe, data, timestamp, SelfServeMessageTypes.TelemetryInfo); sendSelfServeTelemetryMessage(ActionModifiers.Failed, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Failed));
}; };
export const selfServeTraceCancel = (data: SelfServeTelemetryMessage, timestamp?: number): void => { export const traceCancel = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
traceCancel(Action.SelfServe, data, timestamp, SelfServeMessageTypes.TelemetryInfo); sendSelfServeTelemetryMessage(ActionModifiers.Cancel, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Cancel));
};
const sendSelfServeTelemetryMessage = (
actionModifier: string,
data: SelfServeTelemetryMessage,
timeStamp?: number
): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dataToSend: any = {
type: SelfServeMessageTypes.TelemetryInfo,
data: {
action: Action[action],
actionModifier: actionModifier,
data: JSON.stringify(decorateData(data)),
},
};
if (timeStamp) {
dataToSend.data.timeStamp = timeStamp;
}
sendMessage(dataToSend);
};
const decorateData = (data: SelfServeTelemetryMessage, actionModifier?: string) => {
return {
databaseAccountName: userContext.databaseAccount?.name,
defaultExperience: userContext.defaultExperience,
authType: userContext.authType,
subscriptionId: userContext.subscriptionId,
platform: configContext.platform,
env: process.env.NODE_ENV,
actionModifier,
...data,
} as { [key: string]: string };
}; };

View File

@@ -1,5 +1,3 @@
import { TelemetryData } from "../Shared/Telemetry/TelemetryProcessor";
interface BaseInput { interface BaseInput {
dataFieldName: string; dataFieldName: string;
errorMessage?: string; errorMessage?: string;
@@ -89,8 +87,6 @@ export abstract class SelfServeBaseClass {
selfServeDescriptor.initialize = this.initialize; selfServeDescriptor.initialize = this.initialize;
selfServeDescriptor.onSave = this.onSave; selfServeDescriptor.onSave = this.onSave;
selfServeDescriptor.onRefresh = this.onRefresh; selfServeDescriptor.onRefresh = this.onRefresh;
selfServeDescriptor.root.id = className;
return selfServeDescriptor; return selfServeDescriptor;
} }
} }
@@ -162,6 +158,8 @@ export interface RefreshParams {
retryIntervalInMs: number; retryIntervalInMs: number;
} }
export interface SelfServeTelemetryMessage extends TelemetryData { export interface SelfServeTelemetryMessage {
selfServeClassName: string; selfServeClassName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
} }

View File

@@ -150,6 +150,7 @@ describe("SelfServeUtils", () => {
]); ]);
const expectedDescriptor = { const expectedDescriptor = {
root: { root: {
id: "TestClass",
children: [ children: [
{ {
id: "dbThroughput", id: "dbThroughput",
@@ -269,7 +270,7 @@ describe("SelfServeUtils", () => {
"invalidRegions", "invalidRegions",
], ],
}; };
const descriptor = mapToSmartUiDescriptor(context); const descriptor = mapToSmartUiDescriptor("TestClass", context);
expect(descriptor).toEqual(expectedDescriptor); expect(descriptor).toEqual(expectedDescriptor);
}); });
}); });

View File

@@ -112,18 +112,21 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
export const buildSmartUiDescriptor = (className: string, target: unknown): void => { export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>; const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
const smartUiDescriptor = mapToSmartUiDescriptor(context); const smartUiDescriptor = mapToSmartUiDescriptor(className, context);
Reflect.defineMetadata(className, smartUiDescriptor, target); Reflect.defineMetadata(className, smartUiDescriptor, target);
}; };
export const mapToSmartUiDescriptor = (context: Map<string, DecoratorProperties>): SelfServeDescriptor => { export const mapToSmartUiDescriptor = (
className: string,
context: Map<string, DecoratorProperties>
): SelfServeDescriptor => {
const inputNames: string[] = []; const inputNames: string[] = [];
const root = context.get("root"); const root = context.get("root");
context.delete("root"); context.delete("root");
const smartUiDescriptor: SelfServeDescriptor = { const smartUiDescriptor: SelfServeDescriptor = {
root: { root: {
id: undefined, id: className,
info: undefined, info: undefined,
children: [], children: [],
}, },

View File

@@ -1,5 +1,5 @@
import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators"; import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators";
import { selfServeTrace } from "../SelfServeTelemetryProcessor"; import { trace } from "../SelfServeTelemetryProcessor";
import { import {
ChoiceItem, ChoiceItem,
Description, Description,
@@ -177,7 +177,7 @@ export default class SqlX extends SelfServeBaseClass {
currentValues: Map<string, SmartUiInput>, currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput> baselineValues: Map<string, SmartUiInput>
): Promise<OnSaveResult> => { ): Promise<OnSaveResult> => {
selfServeTrace({ selfServeClassName: SqlX.name }); trace({ selfServeClassName: "SqlX" });
const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean; const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean;
const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean;
@@ -234,7 +234,7 @@ export default class SqlX extends SelfServeBaseClass {
portalNotification: { portalNotification: {
initialize: { initialize: {
titleTKey: "CreateInitializeTitle", titleTKey: "CreateInitializeTitle",
messageTKey: "CreateInitializeMessage", messageTKey: "CreateInitializeTitle",
}, },
success: { success: {
titleTKey: "CreateSuccessTitle", titleTKey: "CreateSuccessTitle",

View File

@@ -1,25 +1,15 @@
import { sendMessage } from "../../Common/MessageHandler"; import { sendMessage } from "../../Common/MessageHandler";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { MessageTypes } from "../../Contracts/ExplorerContracts"; import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { SelfServeMessageTypes } from "../../Contracts/SelfServeContracts";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { startTrackEvent, stopTrackEvent, trackEvent } from "../appInsights"; import { appInsights } from "../appInsights";
import { Action, ActionModifiers } from "./TelemetryConstants"; import { Action, ActionModifiers } from "./TelemetryConstants";
// Right now, the ExplorerContracts has MessageTypes as a numeric enum (TelemetryInfo = 0) while the SelfServeContracts type TelemetryData = { [key: string]: unknown };
// has MessageTypes as a string enum (TelemetryInfo = "TelemetryInfo"). We should move to string enums for all use cases.
type TelemetryType = MessageTypes.TelemetryInfo | SelfServeMessageTypes.TelemetryInfo;
export type TelemetryData = { [key: string]: unknown }; export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data: TelemetryData = {}): void {
export function trace(
action: Action,
actionModifier: string = ActionModifiers.Mark,
data: TelemetryData = {},
type: TelemetryType = MessageTypes.TelemetryInfo
): void {
sendMessage({ sendMessage({
type: type, type: MessageTypes.TelemetryInfo,
data: { data: {
action: Action[action], action: Action[action],
actionModifier: actionModifier, actionModifier: actionModifier,
@@ -27,17 +17,13 @@ export function trace(
}, },
}); });
trackEvent({ name: Action[action] }, decorateData(data, actionModifier)); appInsights.trackEvent({ name: Action[action] }, decorateData(data, actionModifier));
} }
export function traceStart( export function traceStart(action: Action, data?: TelemetryData): number {
action: Action,
data?: TelemetryData,
type: TelemetryType = MessageTypes.TelemetryInfo
): number {
const timestamp: number = Date.now(); const timestamp: number = Date.now();
sendMessage({ sendMessage({
type: type, type: MessageTypes.TelemetryInfo,
data: { data: {
action: Action[action], action: Action[action],
actionModifier: ActionModifiers.Start, actionModifier: ActionModifiers.Start,
@@ -46,18 +32,13 @@ export function traceStart(
}, },
}); });
startTrackEvent(Action[action]); appInsights.startTrackEvent(Action[action]);
return timestamp; return timestamp;
} }
export function traceSuccess( export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: number): void {
action: Action,
data?: TelemetryData,
timestamp?: number,
type: TelemetryType = MessageTypes.TelemetryInfo
): void {
sendMessage({ sendMessage({
type: type, type: MessageTypes.TelemetryInfo,
data: { data: {
action: Action[action], action: Action[action],
actionModifier: ActionModifiers.Success, actionModifier: ActionModifiers.Success,
@@ -66,17 +47,12 @@ export function traceSuccess(
}, },
}); });
stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Success)); appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Success));
} }
export function traceFailure( export function traceFailure(action: Action, data?: TelemetryData, timestamp?: number): void {
action: Action,
data?: TelemetryData,
timestamp?: number,
type: TelemetryType = MessageTypes.TelemetryInfo
): void {
sendMessage({ sendMessage({
type: type, type: MessageTypes.TelemetryInfo,
data: { data: {
action: Action[action], action: Action[action],
actionModifier: ActionModifiers.Failed, actionModifier: ActionModifiers.Failed,
@@ -85,17 +61,12 @@ export function traceFailure(
}, },
}); });
stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Failed)); appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Failed));
} }
export function traceCancel( export function traceCancel(action: Action, data?: TelemetryData, timestamp?: number): void {
action: Action,
data?: TelemetryData,
timestamp?: number,
type: TelemetryType = MessageTypes.TelemetryInfo
): void {
sendMessage({ sendMessage({
type: type, type: MessageTypes.TelemetryInfo,
data: { data: {
action: Action[action], action: Action[action],
actionModifier: ActionModifiers.Cancel, actionModifier: ActionModifiers.Cancel,
@@ -104,18 +75,13 @@ export function traceCancel(
}, },
}); });
stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Cancel)); appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Cancel));
} }
export function traceOpen( export function traceOpen(action: Action, data?: TelemetryData, timestamp?: number): number {
action: Action,
data?: TelemetryData,
timestamp?: number,
type: TelemetryType = MessageTypes.TelemetryInfo
): number {
const validTimestamp = timestamp || Date.now(); const validTimestamp = timestamp || Date.now();
sendMessage({ sendMessage({
type: type, type: MessageTypes.TelemetryInfo,
data: { data: {
action: Action[action], action: Action[action],
actionModifier: ActionModifiers.Open, actionModifier: ActionModifiers.Open,
@@ -124,19 +90,14 @@ export function traceOpen(
}, },
}); });
startTrackEvent(Action[action]); appInsights.startTrackEvent(Action[action]);
return validTimestamp; return validTimestamp;
} }
export function traceMark( export function traceMark(action: Action, data?: TelemetryData, timestamp?: number): number {
action: Action,
data?: TelemetryData,
timestamp?: number,
type: TelemetryType = MessageTypes.TelemetryInfo
): number {
const validTimestamp = timestamp || Date.now(); const validTimestamp = timestamp || Date.now();
sendMessage({ sendMessage({
type: type, type: MessageTypes.TelemetryInfo,
data: { data: {
action: Action[action], action: Action[action],
actionModifier: ActionModifiers.Mark, actionModifier: ActionModifiers.Mark,
@@ -145,7 +106,7 @@ export function traceMark(
}, },
}); });
startTrackEvent(Action[action]); appInsights.startTrackEvent(Action[action]);
return validTimestamp; return validTimestamp;
} }

View File

@@ -1,8 +1,5 @@
import { ApplicationInsights } from "@microsoft/applicationinsights-web"; import { ApplicationInsights } from "@microsoft/applicationinsights-web";
// TODO: Remove this after 06/01/21.
// This points to an old app insights instance that is difficult to access
// For now we are sending data to two instances of app insights
const appInsights = new ApplicationInsights({ const appInsights = new ApplicationInsights({
config: { config: {
instrumentationKey: "fa645d97-6237-4656-9559-0ee0cb55ee49", instrumentationKey: "fa645d97-6237-4656-9559-0ee0cb55ee49",
@@ -10,38 +7,7 @@ const appInsights = new ApplicationInsights({
disableCorrelationHeaders: true, disableCorrelationHeaders: true,
}, },
}); });
const appInsights2 = new ApplicationInsights({
config: {
instrumentationKey: "023d2c39-8f86-468e-bb8f-bcaebd9025c7",
disableFetchTracking: false,
disableCorrelationHeaders: true,
},
});
appInsights.loadAppInsights(); appInsights.loadAppInsights();
appInsights.trackPageView(); appInsights.trackPageView(); // Manually call trackPageView to establish the current user/session/pageview
appInsights2.loadAppInsights();
appInsights2.trackPageView();
const trackEvent: typeof appInsights.trackEvent = (...args) => { export { appInsights };
appInsights.trackEvent(...args);
appInsights2.trackEvent(...args);
};
const startTrackEvent: typeof appInsights.startTrackEvent = (...args) => {
appInsights.startTrackEvent(...args);
appInsights2.startTrackEvent(...args);
};
const stopTrackEvent: typeof appInsights.stopTrackEvent = (...args) => {
appInsights.stopTrackEvent(...args);
appInsights2.stopTrackEvent(...args);
};
const trackTrace: typeof appInsights.trackTrace = (...args) => {
appInsights.trackTrace(...args);
appInsights2.trackTrace(...args);
};
export { trackEvent, startTrackEvent, stopTrackEvent, trackTrace };

View File

@@ -3,7 +3,6 @@ import { DatabaseAccount } from "./Contracts/DataModels";
import { SubscriptionType } from "./Contracts/SubscriptionType"; import { SubscriptionType } from "./Contracts/SubscriptionType";
import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType";
import { extractFeatures, Features } from "./Platform/Hosted/extractFeatures"; import { extractFeatures, Features } from "./Platform/Hosted/extractFeatures";
import { CollectionCreation } from "./Shared/Constants";
interface UserContext { interface UserContext {
readonly authType?: AuthType; readonly authType?: AuthType;
@@ -25,8 +24,6 @@ interface UserContext {
readonly isTryCosmosDBSubscription?: boolean; readonly isTryCosmosDBSubscription?: boolean;
readonly portalEnv?: PortalEnv; readonly portalEnv?: PortalEnv;
readonly features: Features; readonly features: Features;
readonly addCollectionFlight: string;
readonly hasWriteAccess: boolean;
} }
type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra"; type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra";
@@ -36,13 +33,10 @@ const features = extractFeatures();
const { enableSDKoperations: useSDKOperations } = features; const { enableSDKoperations: useSDKOperations } = features;
const userContext: UserContext = { const userContext: UserContext = {
hasWriteAccess: true,
isTryCosmosDBSubscription: false, isTryCosmosDBSubscription: false,
portalEnv: "prod", portalEnv: "prod",
features, features,
useSDKOperations, useSDKOperations,
addCollectionFlight: CollectionCreation.DefaultAddCollectionDefaultFlight,
subscriptionType: CollectionCreation.DefaultSubscriptionType,
}; };
function updateUserContext(newContext: Partial<UserContext>): void { function updateUserContext(newContext: Partial<UserContext>): void {

View File

@@ -18,7 +18,6 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet
} }
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function decryptJWTToken(token: string) { export function decryptJWTToken(token: string) {
if (!token) { if (!token) {
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken"); Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");

View File

@@ -1,7 +1,6 @@
import { isInvalidParentFrameOrigin, isReadyMessage } from "./MessageValidation"; import { isInvalidParentFrameOrigin } from "./MessageValidation";
describe("isInvalidParentFrameOrigin", () => { test.each`
test.each`
domain | expected domain | expected
${"https://cosmos.azure.com"} | ${false} ${"https://cosmos.azure.com"} | ${false}
${"https://cosmos.azure.us"} | ${false} ${"https://cosmos.azure.us"} | ${false}
@@ -22,23 +21,6 @@ describe("isInvalidParentFrameOrigin", () => {
${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true} ${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true}
${"https://maliciousazure.com"} | ${true} ${"https://maliciousazure.com"} | ${true}
${"https://maliciousportalsazure.com"} | ${true} ${"https://maliciousportalsazure.com"} | ${true}
`("returns $expected when called with $domain", ({ domain, expected }) => { `("returns $expected when called with $domain", ({ domain, expected }) => {
expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected); expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected);
});
});
describe("isReadyMessage", () => {
test.each`
event | expected
${{ data: { kind: "ready" } }} | ${true}
${{ data: { data: "ready" } }} | ${true}
${{ data: { data: "ready", kind: "ready" } }} | ${true}
${{ data: { kind: "not-ready" } }} | ${false}
${{ data: { data: "not-ready" } }} | ${false}
${{ data: { data: "not-ready", kind: "not-ready" } }} | ${false}
${{ data: {} }} | ${false}
${{}} | ${false}
`("returns $expected when called with $event", ({ event, expected }) => {
expect(isReadyMessage(event as MessageEvent)).toBe(expected);
});
}); });

View File

@@ -20,15 +20,3 @@ function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
console.error(`Invalid parent frame origin detected: ${eventOrigin}`); console.error(`Invalid parent frame origin detected: ${eventOrigin}`);
return false; return false;
} }
export function isReadyMessage(event: MessageEvent): boolean {
if (!event?.data?.kind && !event?.data?.data) {
return false;
}
if (event.data.kind !== "ready" && event.data.data !== "ready") {
return false;
}
return true;
}

View File

@@ -1,23 +0,0 @@
// Adapted from https://gist.github.com/davidgilbertson/ed3c8bb8569bc64b094b87aa88bed5fa
export function copyStyles(sourceDoc: Document, targetDoc: Document): void {
Array.from(sourceDoc.styleSheets).forEach((styleSheet) => {
if (styleSheet.href) {
// for <link> elements loading CSS from a URL
const newLinkEl = sourceDoc.createElement("link");
newLinkEl.rel = "stylesheet";
newLinkEl.href = styleSheet.href;
targetDoc.head.appendChild(newLinkEl);
} else if (styleSheet.cssRules && styleSheet.cssRules.length > 0) {
// for <style> elements
const newStyleEl = sourceDoc.createElement("style");
Array.from(styleSheet.cssRules).forEach((cssRule) => {
// write the text of each rule into the body of the style element
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
});
targetDoc.head.appendChild(newStyleEl);
}
});
}

View File

@@ -3,7 +3,7 @@ import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { AccountKind, DefaultAccountExperience } from "../Common/Constants"; import { AccountKind, DefaultAccountExperience } from "../Common/Constants";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { sendMessage, sendReadyMessage } from "../Common/MessageHandler"; import { sendReadyMessage } from "../Common/MessageHandler";
import { configContext, Platform, updateConfigContext } from "../ConfigContext"; import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
@@ -23,7 +23,6 @@ import {
getDatabaseAccountKindFromExperience, getDatabaseAccountKindFromExperience,
getDatabaseAccountPropertiesFromMetadata, getDatabaseAccountPropertiesFromMetadata,
} from "../Platform/Hosted/HostedUtils"; } from "../Platform/Hosted/HostedUtils";
import { CollectionCreation } from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { PortalEnv, updateUserContext } from "../UserContext"; import { PortalEnv, updateUserContext } from "../UserContext";
import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts"; import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts";
@@ -201,12 +200,11 @@ async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer
if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) { if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) {
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage"); const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
if (initMessage) { if (initMessage) {
const message = JSON.parse(initMessage) as DataExplorerInputsFrame; const message = JSON.parse(initMessage);
console.warn( console.warn(
"Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message" "Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message"
); );
console.dir(message); console.dir(message);
updateContextsFromPortalMessage(message);
const explorer = new Explorer(explorerParams); const explorer = new Explorer(explorerParams);
explorer.configure(message); explorer.configure(message);
resolve(explorer); resolve(explorer);
@@ -238,15 +236,32 @@ async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer
inputs.extensionEndpoint = configContext.PROXY_PATH; inputs.extensionEndpoint = configContext.PROXY_PATH;
} }
updateContextsFromPortalMessage(inputs); const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount;
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId,
portalEnv: inputs.serverId as PortalEnv,
});
const explorer = new Explorer(explorerParams); const explorer = new Explorer(explorerParams);
explorer.configure(inputs); explorer.configure(inputs);
resolve(explorer); resolve(explorer);
if (openAction) { if (openAction) {
handleOpenAction(openAction, explorer.databases(), explorer); handleOpenAction(openAction, explorer.databases(), explorer);
} }
} else if (shouldForwardMessage(message, event.origin)) {
sendMessage(message);
} }
}, },
false false
@@ -256,11 +271,6 @@ async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer
}); });
} }
function shouldForwardMessage(message: PortalMessage, messageOrigin: string) {
// Only allow forwarding messages from the same origin
return messageOrigin === window.document.location.origin && message.type === MessageTypes.TelemetryInfo;
}
function shouldProcessMessage(event: MessageEvent): boolean { function shouldProcessMessage(event: MessageEvent): boolean {
if (typeof event.data !== "object") { if (typeof event.data !== "object") {
return false; return false;
@@ -278,38 +288,6 @@ function shouldProcessMessage(event: MessageEvent): boolean {
return true; return true;
} }
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (
configContext.BACKEND_ENDPOINT &&
configContext.platform === Platform.Portal &&
process.env.NODE_ENV === "development"
) {
inputs.extensionEndpoint = configContext.PROXY_PATH;
}
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount;
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId,
portalEnv: inputs.serverId as PortalEnv,
hasWriteAccess: inputs.hasWriteAccess ?? true,
addCollectionFlight: inputs.addCollectionDefaultFlight || CollectionCreation.DefaultAddCollectionDefaultFlight,
});
}
interface PortalMessage { interface PortalMessage {
openAction?: DataExplorerAction; openAction?: DataExplorerAction;
actionType?: ActionType; actionType?: ActionType;

View File

@@ -1,16 +0,0 @@
import { isObservableArray, Observable, ObservableArray } from "knockout";
import { useEffect, useState } from "react";
export function useObservableState<T>(observable: Observable<T>): [T, (s: T) => void];
export function useObservableState<T>(observable: ObservableArray<T>): [T[], (s: T[]) => void];
export function useObservableState<T>(observable: ObservableArray<T> | Observable<T>): [T | T[], (s: T | T[]) => void] {
const [value, setValue] = useState(observable());
useEffect(() => {
isObservableArray(observable)
? observable.subscribe((values) => setValue([...values]))
: observable.subscribe(setValue);
}, [observable]);
return [value, observable];
}

View File

@@ -1,16 +0,0 @@
import { useState } from "react";
import TabsBase from "../Explorer/Tabs/TabsBase";
import { TabsManager } from "../Explorer/Tabs/TabsManager";
import { useObservableState } from "./useObservableState";
export type UseTabs = {
tabs: readonly TabsBase[];
tabsManager: TabsManager;
};
export function useTabs(): UseTabs {
const [tabsManager] = useState(() => new TabsManager());
const [tabs] = useObservableState(tabsManager.openedTabs);
return { tabs, tabsManager };
}

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