merge master

This commit is contained in:
hardiknai-techm
2021-08-17 08:32:40 +05:30
132 changed files with 8333 additions and 9867 deletions

View File

@@ -1,16 +1 @@
PORTAL_RUNNER_USERNAME=
PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT=
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY=
PORTAL_RUNNER_CONNECTION_STRING=
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html

View File

@@ -142,18 +142,15 @@ src/Explorer/Tree/ResourceTokenCollection.ts
src/Explorer/Tree/StoredProcedure.ts src/Explorer/Tree/StoredProcedure.ts
src/Explorer/Tree/TreeComponents.ts src/Explorer/Tree/TreeComponents.ts
src/Explorer/Tree/Trigger.ts src/Explorer/Tree/Trigger.ts
src/Explorer/Tree/UserDefinedFunction.ts src/Explorer/WaitsForTemplateViewModel.ts
src/GitHub/GitHubClient.test.ts src/GitHub/GitHubClient.test.ts
src/GitHub/GitHubClient.ts src/GitHub/GitHubClient.ts
src/GitHub/GitHubConnector.ts src/GitHub/GitHubConnector.ts
src/GitHub/GitHubContentProvider.test.ts
src/GitHub/GitHubContentProvider.ts
src/GitHub/GitHubOAuthService.ts src/GitHub/GitHubOAuthService.ts
src/Index.ts src/Index.ts
src/Juno/JunoClient.test.ts src/Juno/JunoClient.test.ts
src/Juno/JunoClient.ts src/Juno/JunoClient.ts
src/Platform/Hosted/Authorization.ts src/Platform/Hosted/Authorization.ts
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
src/ReactDevTools.ts src/ReactDevTools.ts
src/Shared/Constants.ts src/Shared/Constants.ts
src/Shared/appInsights.ts src/Shared/appInsights.ts
@@ -191,4 +188,4 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
src/Explorer/Tree/ResourceTreeAdapter.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx
__mocks__/monaco-editor.ts __mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx src/Explorer/Tree/ResourceTree.tsx

View File

@@ -143,7 +143,7 @@ jobs:
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts - ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
- ./test/notebooks/upload.spec.ts # - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:

View File

@@ -1,4 +1,3 @@
{ {
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com", "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
"enableSchemaAnalyzer": true
} }

View File

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 842 B

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,6 +9,7 @@
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @SemiboldFont: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@GrayScale: "grayscale()";
@xSmallFontSize: 4px; @xSmallFontSize: 4px;
@smallFontSize: 8px; @smallFontSize: 8px;

View File

@@ -2,6 +2,7 @@
.dataResourceTree { .dataResourceTree {
margin-left: @MediumSpace; margin-left: @MediumSpace;
overflow: auto;
.databaseHeader { .databaseHeader {
font-size: 14px; font-size: 14px;
@@ -18,6 +19,10 @@
.notebookHeader { .notebookHeader {
font-size: 12px; font-size: 12px;
} }
.clickDisabled {
pointer-events: none;
}
} }

View File

@@ -1,9 +1,7 @@
@import "./Common/Constants"; @import "./Common/Constants";
.resourceTree { .resourceTree {
height: 100%; height: 100%;
width: 20%;
flex: 0 0 auto; flex: 0 0 auto;
.main { .main {
height: 100%; height: 100%;
@@ -46,20 +44,19 @@
} }
.contextmenushowing { .contextmenushowing {
background-color: #EEE; background-color: #eee;
} }
.collectionstree { .collectionstree {
width: 100%; width: 100%;
margin-top: @DefaultSpace; margin-top: @DefaultSpace;
.databaseList { .databaseList {
list-style-type: none; list-style-type: none;
padding-left: 0px; padding-left: 0px;
.collectionList { .collectionList {
padding-left:(2 * @MediumSpace); padding-left: (2 * @MediumSpace);
} }
.collectionChildList { .collectionChildList {
@@ -85,7 +82,7 @@
left: 0px; left: 0px;
float: right; float: right;
display: none; display: none;
padding-left: 6px!important; padding-left: 6px !important;
line-height: @TreeLineHeight; line-height: @TreeLineHeight;
} }
@@ -192,7 +189,7 @@ img.collectionsTreeCollapseExpand {
} }
.expanded::before { .expanded::before {
content: '\23F7'; content: "\23F7";
margin-left: 0px; margin-left: 0px;
font-size: 15px; font-size: 15px;
} }
@@ -211,7 +208,7 @@ img.collectionsTreeCollapseExpand {
transform: rotate(-90deg) translateX(-100%); transform: rotate(-90deg) translateX(-100%);
-webkit-transform: rotate(-90deg) translateX(-100%); -webkit-transform: rotate(-90deg) translateX(-100%);
-ms-transform: rotate(-90deg) translateX(-100%); -ms-transform: rotate(-90deg) translateX(-100%);
border-bottom: 1px solid #CCC; border-bottom: 1px solid #ccc;
} }
.main-nav-img { .main-nav-img {
@@ -257,11 +254,11 @@ ul.nav {
display: none; display: none;
} }
.highlight:hover>.contextmenubutton { .highlight:hover > .contextmenubutton {
display: unset; display: unset;
} }
.highlight:hover>.contextmenubutton::after { .highlight:hover > .contextmenubutton::after {
content: "\2026"; content: "\2026";
font-size: 12px; font-size: 12px;
} }

4642
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,14 +42,15 @@
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3", "@phosphor/widgets": "1.9.3",
"@testing-library/jest-dom": "5.11.9", "@testing-library/jest-dom": "5.11.9",
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "3.0.0",
"clipboard-copy": "4.0.1", "clipboard-copy": "4.0.1",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "9.0.1",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"d3": "6.1.1", "d3": "6.1.1",
@@ -80,10 +81,10 @@
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42", "post-robot": "10.0.42",
"q": "1.5.1", "q": "1.5.1",
"react": "16.13.1", "react": "16.14.0",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
"react-dnd": "9.4.0", "react-dnd": "14.0.2",
"react-dnd-html5-backend": "9.4.0", "react-dnd-html5-backend": "14.0.0",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "11.8.5", "react-i18next": "11.8.5",
@@ -97,7 +98,7 @@
"sanitize-html": "2.3.3", "sanitize-html": "2.3.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "3.1.0", "terser-webpack-plugin": "5.1.4",
"underscore": "1.9.1", "underscore": "1.9.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"zustand": "3.5.0" "zustand": "3.5.0"
@@ -131,6 +132,7 @@
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0", "@typescript-eslint/parser": "4.22.0",
"@webpack-cli/serve": "1.5.2",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"buffer": "5.1.0", "buffer": "5.1.0",
@@ -152,44 +154,45 @@
"html-inline-css-webpack-plugin": "1.11.0", "html-inline-css-webpack-plugin": "1.11.0",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "4.5.2", "html-webpack-plugin": "5.3.2",
"jest": "25.5.4", "jest": "26.6.3",
"jest-canvas-mock": "2.1.0", "jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1", "jest-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "4.1.0", "less-loader": "4.1.0",
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "0.4.3", "mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"playwright": "1.10.0", "playwright": "1.13.0",
"prettier": "2.2.1", "prettier": "2.2.1",
"process": "0.11.10",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4", "react-dev-utils": "11.0.4",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "6.2.2", "ts-loader": "9.2.4",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",
"typedoc": "0.20.36", "typedoc": "0.20.36",
"typescript": "4.3.4", "typescript": "4.3.4",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "4.46.0", "webpack": "5.47.0",
"webpack-bundle-analyzer": "3.6.1", "webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "3.3.10", "webpack-cli": "4.7.2",
"webpack-dev-server": "3.11.0" "webpack-dev-server": "3.11.2"
}, },
"scripts": { "scripts": {
"start": "node --max-old-space-size=10196 node_modules/webpack-dev-server/bin/webpack-dev-server.js", "start": "webpack serve --mode development",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build", "dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci", "build:dataExplorer:ci": "npm run build:ci",
"build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers", "build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
"build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast", "build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
"pack:prod": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode production", "pack:prod": "webpack --mode production",
"pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress", "pack:fast": "webpack --mode development --progress",
"copyToConsumers": "node copyToConsumers", "copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest", "test": "rimraf coverage && jest",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles", "test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",

View File

@@ -94,7 +94,8 @@ export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly SchemaAnalyzer = "schemaanalyzer"; public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -349,6 +350,11 @@ export class Notebook {
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 120000;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -2,17 +2,22 @@ import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg"; import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export interface ResourceTreeProps { export interface ResourceTreeContainerProps {
toggleLeftPaneExpanded: () => void; toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean; isLeftPaneExpanded: boolean;
container: Explorer;
} }
export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
toggleLeftPaneExpanded, toggleLeftPaneExpanded,
isLeftPaneExpanded, isLeftPaneExpanded,
}: ResourceTreeProps): JSX.Element => { container,
}: ResourceTreeContainerProps): JSX.Element => {
return ( return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}> <div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */} {/* Collections Window - - Start */}
@@ -48,9 +53,11 @@ export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
</div> </div>
</div> </div>
{userContext.authType === AuthType.ResourceToken ? ( {userContext.authType === AuthType.ResourceToken ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" /> <ResourceTokenTree />
) : ( ) : userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" /> <div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : (
<ResourceTree container={container} />
)} )}
</div> </div>
{/* Collections Window - End */} {/* Collections Window - End */}

View File

@@ -1,7 +1,10 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
import ko from "knockout";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { Database } from "../../Contracts/ViewModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import { armRequest } from "../../Utils/arm/request"; import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@@ -23,6 +26,15 @@ describe("createCollection", () => {
} as DatabaseAccount, } as DatabaseAccount,
apiType: "SQL", apiType: "SQL",
}); });
useDatabases.setState({
databases: [
{
id: ko.observable("testDatabase"),
loadCollections: () => undefined,
collections: ko.observableArray([]),
} as Database,
],
});
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {

View File

@@ -4,20 +4,16 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
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 { import { getCollectionName } from "../../Utils/APITypeUtils";
createUpdateCassandraTable, import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
getCassandraTable, import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
createUpdateMongoDBCollection,
getMongoDBCollection,
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@@ -59,6 +55,16 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
}; };
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
if (!params.createNewDatabase) {
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
if (!isValid) {
const collectionName = getCollectionName().toLocaleLowerCase();
throw new Error(
`Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists`
);
}
}
const { apiType } = userContext; const { apiType } = userContext;
switch (apiType) { switch (apiType) {
case "SQL": case "SQL":
@@ -77,23 +83,6 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams
}; };
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.SqlContainerResource = { const resource: ARMTypes.SqlContainerResource = {
id: params.collectionId, id: params.collectionId,
@@ -131,23 +120,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }]; const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }];
try {
const getResponse = await getMongoDBCollection(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.MongoDBCollectionResource = { const resource: ARMTypes.MongoDBCollectionResource = {
id: params.collectionId, id: params.collectionId,
@@ -189,23 +161,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams):
}; };
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getCassandraTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.CassandraTableResource = { const resource: ARMTypes.CassandraTableResource = {
id: params.collectionId, id: params.collectionId,
@@ -233,23 +188,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams):
}; };
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getGremlinGraph(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.GremlinGraphResource = { const resource: ARMTypes.GremlinGraphResource = {
id: params.collectionId, id: params.collectionId,
@@ -284,22 +222,6 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise<D
}; };
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.TableResource = { const resource: ARMTypes.TableResource = {
id: params.collectionId, id: params.collectionId,

View File

@@ -2,20 +2,13 @@ import { DatabaseResponse } from "@azure/cosmos";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { import { getDatabaseName } from "../../Utils/APITypeUtils";
createUpdateCassandraKeyspace, import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
getCassandraKeyspace, import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
createUpdateGremlinDatabase,
getGremlinDatabase,
} from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import {
createUpdateMongoDBDatabase,
getMongoDBDatabase,
} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { import {
CassandraKeyspaceCreateUpdateParameters, CassandraKeyspaceCreateUpdateParameters,
CreateUpdateOptions, CreateUpdateOptions,
@@ -48,6 +41,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
} }
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
const databaseName = getDatabaseName().toLocaleLowerCase();
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
}
const { apiType } = userContext; const { apiType } = userContext;
switch (apiType) { switch (apiType) {
@@ -65,22 +63,6 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P
} }
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = { const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: { properties: {
@@ -101,22 +83,6 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
} }
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getMongoDBDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: MongoDBDatabaseCreateUpdateParameters = { const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
properties: { properties: {
@@ -137,22 +103,6 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
} }
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: CassandraKeyspaceCreateUpdateParameters = { const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
properties: { properties: {
@@ -173,22 +123,6 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
} }
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getGremlinDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params); const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: GremlinDatabaseCreateUpdateParameters = { const rpPayload: GremlinDatabaseCreateUpdateParameters = {
properties: { properties: {

View File

@@ -27,7 +27,6 @@ export interface ConfigContext {
hostedExplorerURL: string; hostedExplorerURL: string;
armAPIVersion?: string; armAPIVersion?: string;
allowedJunoOrigins: string[]; allowedJunoOrigins: string[];
enableSchemaAnalyzer: boolean;
msalRedirectURI?: string; msalRedirectURI?: string;
} }
@@ -63,7 +62,6 @@ let configContext: Readonly<ConfigContext> = {
"https://tools-staging.cosmos.azure.com", "https://tools-staging.cosmos.azure.com",
"https://localhost", "https://localhost",
], ],
enableSchemaAnalyzer: false,
}; };
export function resetConfigContext(): void { export function resetConfigContext(): void {

View File

@@ -9,6 +9,7 @@ export interface DatabaseAccount {
export interface DatabaseAccountExtendedProperties { export interface DatabaseAccountExtendedProperties {
documentEndpoint?: string; documentEndpoint?: string;
disableLocalAuth?: boolean;
tableEndpoint?: string; tableEndpoint?: string;
gremlinEndpoint?: string; gremlinEndpoint?: string;
cassandraEndpoint?: string; cassandraEndpoint?: string;

View File

@@ -83,6 +83,7 @@ export const createCollectionContextMenuButton = (
items.push({ items.push({
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {
@@ -109,7 +110,7 @@ export const createCollectionContextMenuButton = (
iconSrc: AddUdfIcon, iconSrc: AddUdfIcon,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
}, },
label: "New UDF", label: "New UDF",
}); });

View File

@@ -23,13 +23,75 @@ export interface DialogState {
dialogProps?: DialogProps; dialogProps?: DialogProps;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
closeDialog: () => void; closeDialog: () => void;
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
) => void;
showOkModalDialog: (title: string, subText: string) => void;
} }
export const useDialog: UseStore<DialogState> = create((set) => ({ export const useDialog: UseStore<DialogState> = create((set, get) => ({
visible: false, visible: false,
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
closeDialog: () => closeDialog: () =>
set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), set(
(state) => ({
visible: false,
openDialog: state.openDialog,
closeDialog: state.closeDialog,
showOkCancelModalDialog: state.showOkCancelModalDialog,
showOkModalDialog: state.showOkModalDialog,
}),
true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above
),
showOkCancelModalDialog: (
title: string,
subText: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
get().closeDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
get().closeDialog();
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
}),
showOkModalDialog: (title: string, subText: string): void =>
get().openDialog({
isModal: true,
title,
subText,
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
get().closeDialog();
},
onSecondaryButtonClick: undefined,
}),
})); }));
export interface TextFieldProps extends ITextFieldProps { export interface TextFieldProps extends ITextFieldProps {

View File

@@ -56,7 +56,7 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
return ( return (
<> <>
<div className={"paneMainContent"}>{content}</div> <div>{content}</div>
{!this.props.showAuthorizeAccess && ( {!this.props.showAuthorizeAccess && (
<> <>
<div className={"paneFooter"} style={ContentFooterStyle}> <div className={"paneFooter"} style={ContentFooterStyle}>

View File

@@ -5,6 +5,9 @@
display: inline-block; display: inline-block;
width: 100%; width: 100%;
.input-type-head-text-field {
width: 100%;
}
textarea { textarea {
width: 100%; width: 100%;
line-height: 1; line-height: 1;
@@ -21,4 +24,11 @@
} }
} }
} }
.input-typeahead-chocies-container {
border: 1px solid lightgrey;
padding: 5px 10px 5px 10px;
cursor: pointer;
.choice-caption{
font-size: 14px;
}
}

View File

@@ -6,14 +6,13 @@
* typeaheadOverrideOptions: { dynamic:false } * typeaheadOverrideOptions: { dynamic:false }
* *
*/ */
import "jquery-typeahead"; import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { KeyCodes } from "../../../Common/Constants";
import "./InputTypeahead.less"; import "./InputTypeahead.less";
export interface Item { export interface Item {
caption: string; caption: string;
value: any; value: string;
} }
/** /**
@@ -75,170 +74,125 @@ export interface InputTypeaheadComponentProps {
useTextarea?: boolean; useTextarea?: boolean;
} }
interface OnClickItem { interface InputTypeaheadComponentState {
matchedKey: string; isSuggestionVisible: boolean;
value: any; selectedChoice: Item;
caption: string; filteredChoices: Item[];
group: string;
} }
interface Cache {
inputValue: string;
selection: Item;
}
interface InputTypeaheadComponentState {}
export class InputTypeaheadComponent extends React.Component< export class InputTypeaheadComponent extends React.Component<
InputTypeaheadComponentProps, InputTypeaheadComponentProps,
InputTypeaheadComponentState InputTypeaheadComponentState
> { > {
private inputElt: HTMLElement; constructor(props: InputTypeaheadComponentProps) {
private containerElt: HTMLElement;
private cache: Cache;
private inputValue: string;
private selection: Item;
public constructor(props: InputTypeaheadComponentProps) {
super(props); super(props);
this.cache = { this.state = {
inputValue: null, isSuggestionVisible: false,
selection: null, filteredChoices: [],
selectedChoice: {
caption: "",
value: "",
},
}; };
} }
/** private onRenderCell = (item: Item): JSX.Element => {
* Props have changed
* @param prevProps
* @param prevState
* @param snapshot
*/
public componentDidUpdate(
prevProps: InputTypeaheadComponentProps,
prevState: InputTypeaheadComponentState,
snapshot: any
): void {
if (prevProps.defaultValue !== this.props.defaultValue) {
$(this.inputElt).val(this.props.defaultValue);
this.initializeTypeahead();
}
}
/**
* Executed once react is done building the DOM for this component
*/
public componentDidMount(): void {
this.initializeTypeahead();
}
public render(): JSX.Element {
return ( return (
<span className="input-typeahead-container"> <div className="input-typeahead-chocies-container" onClick={() => this.onChoiceClick(item)}>
<div <p className="choice-caption">{item.caption}</p>
className="input-typehead" <span>{item.value}</span>
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(event)}
>
<div className="typeahead__container" ref={(input) => (this.containerElt = input)}>
<div className="typeahead__field">
<span className="typeahead__query">
{this.props.useTextarea ? (
<textarea
rows={1}
name="q"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
) : (
<input
name="q"
type="search"
autoComplete="off"
aria-label="Input query"
ref={(input) => (this.inputElt = input)}
defaultValue={this.props.defaultValue}
/>
)}
</span>
{this.props.showSearchButton && (
<span className="typeahead__button">
<button type="submit">
<span className="typeahead__search-icon" />
</button>
</span>
)}
</div> </div>
</div>
</div>
</span>
); );
} };
private onKeyDown(event: React.KeyboardEvent<HTMLElement>) { private onChoiceClick = (item: Item): void => {
if (event.keyCode === KeyCodes.Enter) { this.props.onNewValue(item.caption);
this.setState({ isSuggestionVisible: false, selectedChoice: item });
};
private handleChange = (value: string): void => {
if (!value) {
this.setState({ isSuggestionVisible: true });
}
this.props.onNewValue(value);
const filteredChoices = this.filterChoiceByValue(this.props.choices, value);
this.setState({ filteredChoices });
};
private onSubmit = (event: React.KeyboardEvent<HTMLElement>): void => {
if (event.key === "Enter") {
if (this.props.submitFct) { if (this.props.submitFct) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.props.submitFct(this.cache.inputValue, this.cache.selection); this.props.submitFct(this.props.defaultValue, this.state.selectedChoice);
$(this.containerElt).children(".typeahead__result").hide(); this.setState({ isSuggestionVisible: false });
} }
} }
}
/**
* Must execute once ko is rendered, so that it can find the input element by id
*/
private initializeTypeahead(): void {
const props = this.props;
let cache = this.cache;
let options: any = {
input: this.inputElt,
order: "asc",
minLength: 0,
searchOnFocus: true,
source: {
display: "caption",
data: () => {
return props.choices;
},
},
callback: {
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
cache.selection = item;
if (props.onSelected) {
props.onSelected(item);
}
},
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
cache.inputValue = query;
if (props.onNewValue) {
props.onNewValue(query);
}
},
},
template: (query: string, item: any) => {
// Don't display id if caption *IS* the id
return item.caption === item.value
? "<span>{{caption}}</span>"
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
},
dynamic: true,
}; };
// Override options private filterChoiceByValue = (choices: Item[], searchKeyword: string): Item[] => {
if (props.typeaheadOverrideOptions) { return choices.filter((choice) =>
for (const p in props.typeaheadOverrideOptions) { // @ts-ignore
options[p] = props.typeaheadOverrideOptions[p]; Object.keys(choice).some((key) => choice[key].toLowerCase().includes(searchKeyword.toLowerCase()))
} );
} };
if (props.hasOwnProperty("showCancelButton")) { public render(): JSX.Element {
options.cancelButton = props.showCancelButton; const { defaultValue, useTextarea, placeholder, onNewValue } = this.props;
} const { isSuggestionVisible, selectedChoice, filteredChoices } = this.state;
const theme = getTheme();
$(this.inputElt).typeahead(options); const iconButtonStyles = {
root: {
color: theme.palette.neutralPrimary,
marginLeft: "10px !important",
marginTop: "0px",
marginRight: "2px",
width: "42px",
},
rootHovered: {
color: theme.palette.neutralDark,
},
};
const cancelIcon: IIconProps = { iconName: "cancel" };
const searchIcon: IIconProps = { iconName: "Search" };
return (
<div className="input-typeahead-container">
<Stack horizontal>
<TextField
multiline={useTextarea}
rows={1}
defaultValue={defaultValue}
ariaLabel="Input query"
placeholder={placeholder}
className="input-type-head-text-field"
value={defaultValue}
onKeyDown={this.onSubmit}
onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
/>
{this.props.showCancelButton && (
<IconButton
styles={iconButtonStyles}
iconProps={cancelIcon}
ariaLabel="cancel Button"
onClick={() => onNewValue("")}
/>
)}
{this.props.showSearchButton && (
<IconButton
styles={iconButtonStyles}
iconProps={searchIcon}
ariaLabel="Search Button"
onClick={() => this.props.submitFct(defaultValue, selectedChoice)}
/>
)}
</Stack>
{filteredChoices.length && isSuggestionVisible ? (
<List items={filteredChoices} onRenderCell={this.onRenderCell} />
) : undefined}
</div>
);
} }
} }

View File

@@ -1,61 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`inputTypeahead renders <input /> 1`] = ` exports[`inputTypeahead renders <input /> 1`] = `
<span <div
className="input-typeahead-container" className="input-typeahead-container"
> >
<div <Stack
className="input-typehead" horizontal={true}
>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={false}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
> placeholder="placeholder"
<div rows={1}
className="typeahead__container"
>
<div
className="typeahead__field"
>
<span
className="typeahead__query"
>
<input
aria-label="Input query"
autoComplete="off"
name="q"
type="search"
/> />
</span> </Stack>
</div> </div>
</div>
</div>
</span>
`; `;
exports[`inputTypeahead renders <textarea /> 1`] = ` exports[`inputTypeahead renders <textarea /> 1`] = `
<span <div
className="input-typeahead-container" className="input-typeahead-container"
> >
<div <Stack
className="input-typehead" horizontal={true}
>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={true}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
> placeholder="placeholder"
<div
className="typeahead__container"
>
<div
className="typeahead__field"
>
<span
className="typeahead__query"
>
<textarea
aria-label="Input query"
autoComplete="off"
name="q"
rows={1} rows={1}
/> />
</span> </Stack>
</div> </div>
</div>
</div>
</span>
`; `;

View File

@@ -29,6 +29,7 @@ import { QueriesClient } from "../../../Common/QueriesClient";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels 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 { useDialog } from "../Dialog";
const title = "Open Saved Queries"; const title = "Open Saved Queries";
@@ -222,7 +223,11 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
key: "Delete", key: "Delete",
text: "Delete query", text: "Delete query",
onClick: async () => { onClick: async () => {
if (window.confirm("Are you sure you want to delete this query?")) { useDialog.getState().showOkCancelModalDialog(
"Confirm delete",
"Are you sure you want to delete this query?",
"Delete",
async () => {
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: title,
@@ -250,7 +255,10 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
); );
} }
await this.fetchSavedQueries(); // get latest state await this.fetchSavedQueries(); // get latest state
} },
"Cancel",
undefined
);
}, },
}, },
], ],

View File

@@ -44,10 +44,6 @@ exports[`SettingsComponent renders 1`] = `
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
@@ -115,10 +111,6 @@ exports[`SettingsComponent renders 1`] = `
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],

View File

@@ -17,7 +17,6 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]), collections: ko.observableArray<Collection>([collection]),
} as Database; } as Database;
const explorer = {} as Explorer; const explorer = {} as Explorer;
explorer.showOkModalDialog = () => {};
useDatabases.getState().addDatabases([database]); useDatabases.getState().addDatabases([database]);
const dataSamplesUtil = new DataSamplesUtil(explorer); const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,6 +1,7 @@
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
@@ -20,7 +21,7 @@ export class DataSamplesUtil {
const containerName = generator.getCollectionId(); const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg); logConsoleError(msg);
return; return;
} }
@@ -29,7 +30,7 @@ export class DataSamplesUtil {
.createSampleContainerAsync() .createSampleContainerAsync()
.catch((error) => logConsoleError(`Error creating sample container: ${error}`)); .catch((error) => logConsoleError(`Error creating sample container: ${error}`));
const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleInfo(msg); logConsoleInfo(msg);
} }

View File

@@ -1,4 +1,3 @@
import { IChoiceGroupProps } from "@fluentui/react";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
@@ -11,7 +10,6 @@ import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient"; import { QueriesClient } from "../Common/QueriesClient";
import { configContext } from "../ConfigContext";
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 { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
@@ -35,7 +33,8 @@ import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog"; import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import * as FileSystemUtil from "./Notebook/FileSystemUtil";
@@ -59,7 +58,6 @@ import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import { useDatabases } from "./useDatabases"; import { useDatabases } from "./useDatabases";
import { useSelectedNode } from "./useSelectedNode"; import { useSelectedNode } from "./useSelectedNode";
@@ -74,9 +72,6 @@ export default class Explorer {
// Resource Tree // Resource Tree
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
// Resource Token
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
// Tabs // Tabs
public isTabsContentExpanded: ko.Observable<boolean>; public isTabsContentExpanded: ko.Observable<boolean>;
@@ -186,7 +181,6 @@ export default class Explorer {
); );
this.resourceTree = new ResourceTreeAdapter(this); this.resourceTree = new ResourceTreeAdapter(this);
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
// Override notebook server parameters from URL parameters // Override notebook server parameters from URL parameters
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) { if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
@@ -213,10 +207,6 @@ export default class Explorer {
}); });
} }
if (configContext.enableSchemaAnalyzer) {
userContext.features.enableSchemaAnalyzer = true;
}
this.refreshExplorer(); this.refreshExplorer();
} }
@@ -366,6 +356,9 @@ export default class Explorer {
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken, authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
}); });
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.refreshNotebookList(); this.refreshNotebookList();
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
@@ -544,17 +537,22 @@ export default class Explorer {
} }
} }
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(
name: string,
content: string,
parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled"; const error = "Attempt to upload notebook, but notebook is not enabled";
handleError(error, "Explorer/uploadFile"); handleError(error, "Explorer/uploadFile");
throw new Error(error); throw new Error(error);
} }
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree);
promise promise
.then(() => this.resourceTree.triggerRender()) .then(() => this.resourceTree.triggerRender())
.catch((reason) => this.showOkModalDialog("Unable to upload file", reason)); .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason)));
return promise; return promise;
} }
@@ -620,51 +618,6 @@ export default class Explorer {
this.notebookManager?.openCopyNotebookPane(name, content); this.notebookManager?.openCopyNotebookPane(name, content);
} }
public showOkModalDialog(title: string, msg: string): void {
useDialog.getState().openDialog({
isModal: true,
title,
subText: msg,
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
useDialog.getState().closeDialog();
},
onSecondaryButtonClick: undefined,
});
}
public showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean
): void {
useDialog.getState().openDialog({
isModal: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
useDialog.getState().closeDialog();
onOk && onOk();
},
onSecondaryButtonClick: () => {
useDialog.getState().closeDialog();
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled: isPrimaryButtonDisabled,
});
}
/** /**
* Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree.
* Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder.
@@ -724,7 +677,7 @@ export default class Explorer {
return true; return true;
} }
public renameNotebook(notebookFile: NotebookContentItem): void { public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled"; const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook"); handleError(error, "Explorer/renameNotebook");
@@ -738,7 +691,9 @@ export default class Explorer {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
}); });
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); useDialog
.getState()
.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else { } else {
useSidePanel.getState().openSidePanel( useSidePanel.getState().openSidePanel(
"Rename Notebook", "Rename Notebook",
@@ -755,7 +710,7 @@ export default class Explorer {
paneTitle="Rename Notebook" paneTitle="Rename Notebook"
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> => onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree)
} }
notebookFile={notebookFile} notebookFile={notebookFile}
/> />
@@ -763,7 +718,7 @@ export default class Explorer {
} }
} }
public onCreateDirectory(parent: NotebookContentItem): void { public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled"; const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory"); handleError(error, "Explorer/onCreateDirectory");
@@ -785,7 +740,7 @@ export default class Explorer {
submitButtonLabel="Create" submitButtonLabel="Create"
defaultInput="" defaultInput=""
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> => onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input) this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree)
} }
notebookFile={parent} notebookFile={parent}
/> />
@@ -846,13 +801,15 @@ export default class Explorer {
} }
await this.resourceTree.initialize(); await this.resourceTree.initialize();
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.notebookManager?.refreshPinnedRepos(); this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) { if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
} }
}; };
public deleteNotebookFile(item: NotebookContentItem): Promise<void> { public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to delete notebook file, but notebook is not enabled"; const error = "Attempt to delete notebook file, but notebook is not enabled";
handleError(error, "Explorer/deleteNotebookFile"); handleError(error, "Explorer/deleteNotebookFile");
@@ -866,7 +823,9 @@ export default class Explorer {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
}); });
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); useDialog
.getState()
.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
return Promise.reject(); return Promise.reject();
} }
@@ -883,7 +842,7 @@ export default class Explorer {
return Promise.reject(); return Promise.reject();
} }
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then(
() => logConsoleInfo(`Successfully deleted: ${item.path}`), () => logConsoleInfo(`Successfully deleted: ${item.path}`),
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`) (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
); );
@@ -892,7 +851,7 @@ export default class Explorer {
/** /**
* This creates a new notebook file, then opens the notebook * This creates a new notebook file, then opens the notebook
*/ */
public onNewNotebookClicked(parent?: NotebookContentItem): void { public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled"; const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked"); handleError(error, "Explorer/onNewNotebookClicked");
@@ -907,7 +866,7 @@ export default class Explorer {
}); });
this.notebookManager?.notebookContentClient this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent) .createNewNotebookFile(parent, isGithubTree)
.then((newFile: NotebookContentItem) => { .then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`); logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -936,14 +895,15 @@ export default class Explorer {
.finally(clearInProgressMessage); .finally(clearInProgressMessage);
} }
public refreshContentItem(item: NotebookContentItem): Promise<void> { // TODO: Delete this function when ResourceTreeAdapter is removed.
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled"; const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem"); handleError(error, "Explorer/refreshContentItem");
return Promise.reject(new Error(error)); return Promise.reject(new Error(error));
} }
return this.notebookManager?.notebookContentClient.updateItemChildren(item); await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
} }
public openNotebookTerminal(kind: ViewModels.TerminalKind) { public openNotebookTerminal(kind: ViewModels.TerminalKind) {
@@ -1124,6 +1084,7 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
if (!userContext.features.notebooksTemporarilyDown) {
if (isNotebookEnabled) { if (isNotebookEnabled) {
await this.initNotebooks(userContext.databaseAccount); await this.initNotebooks(userContext.databaseAccount);
} else if (this.notebookToImport) { } else if (this.notebookToImport) {
@@ -1131,4 +1092,5 @@ export default class Explorer {
this._openSetupNotebooksPaneForQuickstart(); this._openSetupNotebooksPaneForQuickstart();
} }
} }
}
} }

View File

@@ -6,9 +6,9 @@
import * as React from "react"; import * as React from "react";
import CancelIcon from "../../../../images/cancel.svg"; import CancelIcon from "../../../../images/cancel.svg";
import CheckIcon from "../../../../images/check.svg"; import CheckIcon from "../../../../images/check-1.svg";
import DeleteIcon from "../../../../images/delete.svg"; import DeleteIcon from "../../../../images/delete.svg";
import EditIcon from "../../../../images/edit.svg"; import EditIcon from "../../../../images/edit-1.svg";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel"; import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";

View File

@@ -103,19 +103,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(false); //TODO: modify once notebooks are available
expect(enableNotebookBtn.tooltipText).toBe(""); expect(enableNotebookBtn).toBeUndefined();
//expect(enableNotebookBtn).toBeDefined();
//expect(enableNotebookBtn.disabled).toBe(false);
//expect(enableNotebookBtn.tooltipText).toBe("");
}); });
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(true); //TODO: modify once notebooks are available
expect(enableNotebookBtn.tooltipText).toBe( expect(enableNotebookBtn).toBeUndefined();
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks." //expect(enableNotebookBtn).toBeDefined();
); //expect(enableNotebookBtn.disabled).toBe(true);
//expect(enableNotebookBtn.tooltipText).toBe(
// "Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
//);
}); });
}); });
@@ -192,8 +198,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
@@ -203,8 +212,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
@@ -290,9 +302,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
@@ -302,8 +318,11 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.tooltipText).toBe(""); //TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
}); });

View File

@@ -22,6 +22,7 @@ import * as Constants from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
@@ -66,35 +67,50 @@ export function createStaticCommandBarButtons(
newCollectionBtn.children.push(newDatabaseBtn); newCollectionBtn.children.push(newDatabaseBtn);
} }
buttons.push(createDivider());
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled) {
buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container); const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)]; newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
buttons.push(newNotebookButton); notebookButtons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) { if (container.notebookManager?.gitHubOAuthService) {
buttons.push(createManageGitHubAccountButton(container)); notebookButtons.push(createManageGitHubAccountButton(container));
} }
buttons.push(createOpenTerminalButton(container)); notebookButtons.push(createOpenTerminalButton(container));
buttons.push(createNotebookWorkspaceResetButton(container)); notebookButtons.push(createNotebookWorkspaceResetButton(container));
if ( if (
(userContext.apiType === "Mongo" && (userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled && useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) || selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra" userContext.apiType === "Cassandra"
) { ) {
buttons.push(createDivider()); notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") { if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container)); notebookButtons.push(createOpenCassandraTerminalButton(container));
} else { } else {
buttons.push(createOpenMongoTerminalButton(container)); notebookButtons.push(createOpenMongoTerminalButton(container));
} }
} }
notebookButtons.forEach((btn) => {
if (userContext.features.notebooksTemporarilyDown) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
} else { } else {
if (!isRunningOnNationalCloud()) { applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
}
buttons.push(btn);
});
} else {
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container)); buttons.push(createEnableNotebooksButton(container));
} }
} }
@@ -151,7 +167,9 @@ export function createContextCommandBarButtons(
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {
if (!userContext.features.notebooksTemporarilyDown) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
@@ -159,7 +177,13 @@ export function createContextCommandBarButtons(
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo", tooltipText:
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
}; };
buttons.push(newMongoShellBtn); buttons.push(newMongoShellBtn);
} }
@@ -387,6 +411,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
return buttons; return buttons;
} }
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps { function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook"; const label = "New Notebook";
return { return {
@@ -560,6 +591,7 @@ function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonC
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps { function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn(); const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub"; const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
const junoClient = new JunoClient();
return { return {
iconSrc: GitHubIcon, iconSrc: GitHubIcon,
iconAlt: label, iconAlt: label,
@@ -568,7 +600,11 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
.getState() .getState()
.openSidePanel( .openSidePanel(
label, label,
<GitHubReposPanel explorer={container} gitHubClientProp={container.notebookManager.gitHubClient} /> <GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>
), ),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,

View File

@@ -22,6 +22,13 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => { export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight; const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
const getFilter = (isDisabled: boolean): string => {
if (isDisabled) {
return StyleConstants.GrayScale;
}
return undefined;
};
return btns return btns
.filter((btn) => btn) .filter((btn) => btn)
.map( .map(
@@ -37,6 +44,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
style: { style: {
width: StyleConstants.CommandBarIconWidth, // 16 width: StyleConstants.CommandBarIconWidth, // 16
alignSelf: btn.iconName ? "baseline" : undefined, alignSelf: btn.iconName ? "baseline" : undefined,
filter: getFilter(btn.disabled),
}, },
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined, imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName, iconName: btn.iconName,
@@ -123,8 +131,12 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
width: 12, width: 12,
paddingLeft: 1, paddingLeft: 1,
paddingTop: 6, paddingTop: 6,
filter: getFilter(btn.disabled),
},
imageProps: {
src: ChevronDownIcon,
alt: btn.iconAlt,
}, },
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt },
}; };
} }

View File

@@ -6,7 +6,7 @@ import { Dropdown, IDropdownOption } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import AnimateHeight from "react-animate-height"; import AnimateHeight from "react-animate-height";
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif"; import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
import ClearIcon from "../../../../images/Clear.svg"; import ClearIcon from "../../../../images/Clear-1.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg";
import ErrorRedIcon from "../../../../images/error_red.svg"; import ErrorRedIcon from "../../../../images/error_red.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";

View File

@@ -277,6 +277,10 @@ export class NotebookComponentBootstrapper {
return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>); return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>);
} }
public isNotebookUntrusted(): boolean {
return NotebookUtil.isNotebookUntrusted(this.getStore().getState(), this.contentRef);
}
/** /**
* For display purposes, only return non-killed kernels * For display purposes, only return non-killed kernels
*/ */

View File

@@ -1,12 +1,14 @@
import { AppState, ContentRef, selectors } from "@nteract/core"; import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps { interface VirtualCommandBarComponentProps {
kernelSpecName: string; kernelSpecName: string;
kernelStatus: string; kernelStatus: string;
currentCellType: string; currentCellType: string;
isNotebookUntrusted: boolean;
onRender: () => void; onRender: () => void;
} }
@@ -20,7 +22,8 @@ class VirtualCommandBarComponent extends React.Component<VirtualCommandBarCompon
return ( return (
this.props.kernelStatus !== nextProps.kernelStatus || this.props.kernelStatus !== nextProps.kernelStatus ||
this.props.kernelSpecName !== nextProps.kernelSpecName || this.props.kernelSpecName !== nextProps.kernelSpecName ||
this.props.currentCellType !== nextProps.currentCellType this.props.currentCellType !== nextProps.currentCellType ||
this.props.isNotebookUntrusted !== nextProps.isNotebookUntrusted
); );
} }
@@ -50,6 +53,7 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
} as VirtualCommandBarComponentProps; } as VirtualCommandBarComponentProps;
} }
@@ -69,6 +73,7 @@ const makeMapStateToProps = (
kernelStatus, kernelStatus,
kernelSpecName, kernelSpecName,
currentCellType, currentCellType,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
onRender: initialProps.onRender, onRender: initialProps.onRender,
}; };
}; };

View File

@@ -38,6 +38,7 @@ import { useTabs } from "../../../hooks/useTabs";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { useDialog } from "../../Controls/Dialog";
import * as FileSystemUtil from "../FileSystemUtil"; import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
@@ -686,10 +687,8 @@ const handleKernelConnectionLostEpic = (
logConsoleError(msg); logConsoleError(msg);
logFailureToTelemetry(state, "Kernel restart error", msg); logFailureToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer; useDialog.getState().showOkModalDialog("kernel restarts", msg);
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
return of(EMPTY); return of(EMPTY);
} }
@@ -773,8 +772,7 @@ const closeUnsupportedMimetypesEpic = (
ofType(actions.FETCH_CONTENT_FULFILLED), ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap((action) => { mergeMap((action) => {
const mimetype = action.payload.model.mimetype; const mimetype = action.payload.model.mimetype;
const explorer = window.dataExplorer; if (!TextFile.handles(mimetype)) {
if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
useTabs useTabs
@@ -783,7 +781,7 @@ const closeUnsupportedMimetypesEpic = (
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
); );
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg); useDialog.getState().showOkModalDialog("File cannot be rendered", msg);
logConsoleError(msg); logConsoleError(msg);
} }
return EMPTY; return EMPTY;
@@ -803,8 +801,6 @@ const closeContentFailedToFetchEpic = (
return action$.pipe( return action$.pipe(
ofType(actions.FETCH_CONTENT_FAILED), ofType(actions.FETCH_CONTENT_FAILED),
mergeMap((action) => { mergeMap((action) => {
const explorer = window.dataExplorer;
if (explorer) {
const filepath = action.payload.filepath; const filepath = action.payload.filepath;
// Close tab and show error message // Close tab and show error message
useTabs useTabs
@@ -813,9 +809,8 @@ const closeContentFailedToFetchEpic = (
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
); );
const msg = `Failed to load file: ${filepath}.`; const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg); useDialog.getState().showOkModalDialog("Failure to load", msg);
logConsoleError(msg); logConsoleError(msg);
}
return EMPTY; return EMPTY;
}) })
); );

View File

@@ -1,5 +1,6 @@
import { stringifyNotebook } from "@nteract/commutable"; import { stringifyNotebook } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core"; import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
import { cloneDeep } from "lodash";
import { AjaxResponse } from "rxjs/ajax"; import { AjaxResponse } from "rxjs/ajax";
import * as StringUtils from "../../Utils/StringUtils"; import * as StringUtils from "../../Utils/StringUtils";
import * as FileSystemUtil from "./FileSystemUtil"; import * as FileSystemUtil from "./FileSystemUtil";
@@ -14,7 +15,17 @@ export class NotebookContentClient {
* This updates the item and points all the children's parent to this item * This updates the item and points all the children's parent to this item
* @param item * @param item
*/ */
public updateItemChildren(item: NotebookContentItem): Promise<void> { public async updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
const subItems = await this.fetchNotebookFiles(item.path);
const clonedItem = cloneDeep(item);
subItems.forEach((subItem) => (subItem.parent = clonedItem));
clonedItem.children = subItems;
return clonedItem;
}
// TODO: Delete this function when ResourceTreeAdapter is removed.
public async updateItemChildrenInPlace(item: NotebookContentItem): Promise<void> {
return this.fetchNotebookFiles(item.path).then((subItems) => { return this.fetchNotebookFiles(item.path).then((subItems) => {
item.children = subItems; item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item)); subItems.forEach((subItem) => (subItem.parent = item));
@@ -25,7 +36,7 @@ export class NotebookContentClient {
* *
* @param parent parent folder * @param parent parent folder
*/ */
public createNewNotebookFile(parent: NotebookContentItem): Promise<NotebookContentItem> { public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
} }
@@ -46,6 +57,8 @@ export class NotebookContentClient {
const notebookFile = xhr.response; const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) { if (parent.children) {
item.parent = parent; item.parent = parent;
parent.children.push(item); parent.children.push(item);
@@ -55,8 +68,11 @@ export class NotebookContentClient {
}); });
} }
public deleteContentItem(item: NotebookContentItem): Promise<void> { public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
return this.deleteNotebookFile(item.path).then((path: string) => { const path = await this.deleteNotebookFile(item.path);
useNotebook.getState().deleteNotebookItem(item, isGithubTree);
// TODO: Delete once old resource tree is removed
if (!path || path !== item.path) { if (!path || path !== item.path) {
throw new Error("No path provided"); throw new Error("No path provided");
} }
@@ -66,7 +82,6 @@ export class NotebookContentClient {
const newChildren = item.parent.children.filter((child) => child.path !== path); const newChildren = item.parent.children.filter((child) => child.path !== path);
item.parent.children = newChildren; item.parent.children = newChildren;
} }
});
} }
/** /**
@@ -78,7 +93,8 @@ export class NotebookContentClient {
public async uploadFileAsync( public async uploadFileAsync(
name: string, name: string,
content: string, content: string,
parent: NotebookContentItem parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> { ): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) { if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`); throw new Error(`Parent must be a directory: ${parent}`);
@@ -102,6 +118,8 @@ export class NotebookContentClient {
.then((xhr: AjaxResponse) => { .then((xhr: AjaxResponse) => {
const notebookFile = xhr.response; const notebookFile = xhr.response;
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
if (parent.children) { if (parent.children) {
item.parent = parent; item.parent = parent;
parent.children.push(item); parent.children.push(item);
@@ -124,7 +142,11 @@ export class NotebookContentClient {
* @param sourcePath * @param sourcePath
* @param targetName is not prefixed with path * @param targetName is not prefixed with path
*/ */
public renameNotebook(item: NotebookContentItem, targetName: string): Promise<NotebookContentItem> { public renameNotebook(
item: NotebookContentItem,
targetName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
const sourcePath = item.path; const sourcePath = item.path;
// Match extension // Match extension
if (sourcePath.indexOf(".") !== -1) { if (sourcePath.indexOf(".") !== -1) {
@@ -150,6 +172,9 @@ export class NotebookContentClient {
item.name = notebookFile.name; item.name = notebookFile.name;
item.path = notebookFile.path; item.path = notebookFile.path;
item.timestamp = NotebookUtil.getCurrentTimestamp(); item.timestamp = NotebookUtil.getCurrentTimestamp();
useNotebook.getState().updateNotebookItem(item, isGithubTree);
return item; return item;
}); });
} }
@@ -159,7 +184,11 @@ export class NotebookContentClient {
* @param parent * @param parent
* @param newDirectoryName basename of the new directory * @param newDirectoryName basename of the new directory
*/ */
public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise<NotebookContentItem> { public async createDirectory(
parent: NotebookContentItem,
newDirectoryName: string,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
if (parent.type !== NotebookContentItemType.Directory) { if (parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent is not a directory: ${parent.path}`); throw new Error(`Parent is not a directory: ${parent.path}`);
} }
@@ -186,8 +215,11 @@ export class NotebookContentClient {
const dir = xhr.response; const dir = xhr.response;
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
// TODO: delete when ResourceTreeAdapter is removed
item.parent = parent; item.parent = parent;
parent.children?.push(item); parent.children?.push(item);
return item; return item;
}); });
} }

View File

@@ -18,6 +18,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
@@ -29,6 +30,7 @@ import { SnapshotRequest } from "./NotebookComponent/types";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils"; import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
import { useNotebook } from "./useNotebook";
type NotebookPaneContent = string | ImmutableNotebook; type NotebookPaneContent = string | ImmutableNotebook;
@@ -89,6 +91,7 @@ export default class NotebookManager {
this.gitHubClient.setToken(token?.access_token); this.gitHubClient.setToken(token?.access_token);
if (this?.gitHubOAuthService.isLoggedIn()) { if (this?.gitHubOAuthService.isLoggedIn()) {
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
setTimeout(() => {
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
@@ -99,6 +102,7 @@ export default class NotebookManager {
junoClientProp={this.junoClient} junoClientProp={this.junoClient}
/> />
); );
}, 200);
} }
this.params.refreshCommandBarButtons(); this.params.refreshCommandBarButtons();
@@ -108,6 +112,7 @@ export default class NotebookManager {
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender(); this.params.resourceTree.triggerRender();
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
}); });
this.refreshPinnedRepos(); this.refreshPinnedRepos();
} }
@@ -139,6 +144,7 @@ export default class NotebookManager {
notebookContentRef={notebookContentRef} notebookContentRef={notebookContentRef}
onTakeSnapshot={onTakeSnapshot} onTakeSnapshot={onTakeSnapshot}
/>, />,
"440px",
onClosePanel onClosePanel
); );
} }
@@ -167,7 +173,9 @@ export default class NotebookManager {
if (error.status === HttpStatusCodes.Unauthorized) { if (error.status === HttpStatusCodes.Unauthorized) {
this.gitHubOAuthService.resetToken(); this.gitHubOAuthService.resetToken();
this.params.container.showOkCancelModalDialog( useDialog
.getState()
.showOkCancelModalDialog(
undefined, undefined,
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
"Connect to GitHub", "Connect to GitHub",
@@ -179,6 +187,7 @@ export default class NotebookManager {
<GitHubReposPanel <GitHubReposPanel
explorer={this.params.container} explorer={this.params.container}
gitHubClientProp={this.params.container.notebookManager.gitHubClient} gitHubClientProp={this.params.container.notebookManager.gitHubClient}
junoClientProp={this.junoClient}
/> />
), ),
"Cancel", "Cancel",
@@ -190,7 +199,7 @@ export default class NotebookManager {
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => { private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
let commitMsg = "Committed from Azure Cosmos DB Notebooks"; let commitMsg = "Committed from Azure Cosmos DB Notebooks";
this.params.container.showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
title || "Commit", title || "Commit",
undefined, undefined,
primaryButtonLabel || "Commit", primaryButtonLabel || "Commit",

View File

@@ -6,7 +6,7 @@ import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
import * as React from "react"; import * as React from "react";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
@@ -14,6 +14,7 @@ import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform"; import loadTransform from "../NotebookComponent/loadTransform";
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types"; import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
import { AzureTheme } from "./AzureTheme"; import { AzureTheme } from "./AzureTheme";
import "./base.css"; import "./base.css";
import CellCreator from "./decorators/CellCreator"; import CellCreator from "./decorators/CellCreator";
@@ -107,6 +108,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
return ( return (
<> <>
<div className="NotebookRendererContainer"> <div className="NotebookRendererContainer">
<SecurityWarningBar contentRef={this.props.contentRef} />
<div className="NotebookRenderer" ref={this.notebookRendererRef}> <div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}> <KeyboardShortcuts contentRef={this.props.contentRef}>

View File

@@ -19,6 +19,12 @@
} }
} }
.disabledRunCellButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh;
}
}
.greyStopButton { .greyStopButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon { .runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh; color: @BaseMediumHigh;

View File

@@ -5,6 +5,7 @@ import { Dispatch } from "redux";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { CdbAppState } from "../NotebookComponent/types"; import { CdbAppState } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
export interface PassedPromptProps { export interface PassedPromptProps {
id: string; id: string;
@@ -12,6 +13,7 @@ export interface PassedPromptProps {
status?: string; status?: string;
executionCount?: number; executionCount?: number;
isHovered?: boolean; isHovered?: boolean;
isRunDisabled?: boolean;
runCell?: () => void; runCell?: () => void;
stopCell?: () => void; stopCell?: () => void;
} }
@@ -20,6 +22,7 @@ interface ComponentProps {
id: string; id: string;
contentRef: ContentRef; contentRef: ContentRef;
isHovered?: boolean; isHovered?: boolean;
isNotebookUntrusted?: boolean;
children: (props: PassedPromptProps) => React.ReactNode; children: (props: PassedPromptProps) => React.ReactNode;
} }
@@ -47,6 +50,7 @@ export class PromptPure extends React.Component<Props> {
runCell: this.props.executeCell, runCell: this.props.executeCell,
stopCell: this.props.stopExecution, stopCell: this.props.stopExecution,
isHovered: this.props.isHovered, isHovered: this.props.isHovered,
isRunDisabled: this.props.isNotebookUntrusted,
})} })}
</div> </div>
); );
@@ -75,6 +79,7 @@ const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((s
status, status,
executionCount, executionCount,
isHovered, isHovered,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -0,0 +1,27 @@
import { shallow } from "enzyme";
import { PassedPromptProps } from "./Prompt";
import { promptContent } from "./PromptContent";
describe("PromptContent", () => {
it("renders for busy status", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
status: "busy",
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
it("renders when hovered", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
isHovered: true,
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,5 +1,6 @@
import { IconButton, Spinner, SpinnerSize } from "@fluentui/react"; import { IconButton, Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NotebookUtil } from "../NotebookUtil";
import { PassedPromptProps } from "./Prompt"; import { PassedPromptProps } from "./Prompt";
import "./Prompt.less"; import "./Prompt.less";
@@ -23,15 +24,18 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
</div> </div>
); );
} else if (props.isHovered) { } else if (props.isHovered) {
const playButtonText = "Run cell"; const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell";
return ( return (
<div className={props.isRunDisabled ? "disabledRunCellButton" : ""}>
<IconButton <IconButton
className="runCellButton" className="runCellButton"
iconProps={{ iconName: "MSNVideosSolid" }} iconProps={{ iconName: "MSNVideosSolid" }}
title={playButtonText} title={playButtonText}
ariaLabel={playButtonText} ariaLabel={playButtonText}
disabled={props.isRunDisabled}
onClick={props.runCell} onClick={props.runCell}
/> />
</div>
); );
} else { } else {
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>; return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;

View File

@@ -79,7 +79,7 @@ interface InitialProps {
contentRef: ContentRef; contentRef: ContentRef;
} }
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => { const makeMapStateToProps = (_initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
const { contentRef } = initialProps; const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
@@ -101,14 +101,14 @@ const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps)
const lastSaved = content && content.lastSaved ? content.lastSaved : undefined; const lastSaved = content && content.lastSaved ? content.lastSaved : undefined;
const kernelStatus = kernel !== undefined && kernel.status !== undefined ? kernel.status : NOT_CONNECTED; const kernelStatus = kernel?.status || NOT_CONNECTED;
// TODO: We need kernels associated to the kernelspec they came from // TODO: We need kernels associated to the kernelspec they came from
// so we can pluck off the display_name and provide it here // so we can pluck off the display_name and provide it here
let kernelSpecDisplayName = " "; let kernelSpecDisplayName = " ";
if (kernelStatus === NOT_CONNECTED) { if (kernelStatus === NOT_CONNECTED) {
kernelSpecDisplayName = "no kernel"; kernelSpecDisplayName = "no kernel";
} else if (kernel !== undefined && kernel.kernelSpecName !== undefined) { } else if (kernel?.kernelSpecName) {
kernelSpecDisplayName = kernel.kernelSpecName; kernelSpecDisplayName = kernel.kernelSpecName;
} else if (content && content.type === "notebook") { } else if (content && content.type === "notebook") {
kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " "; kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " ";

View File

@@ -36,6 +36,7 @@ interface StateProps {
cellIdAbove: CellId; cellIdAbove: CellId;
cellIdBelow: CellId; cellIdBelow: CellId;
hasCodeOutput: boolean; hasCodeOutput: boolean;
isNotebookUntrusted: boolean;
} }
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> { class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
@@ -43,12 +44,16 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
render(): JSX.Element { render(): JSX.Element {
let items: IContextualMenuItem[] = []; let items: IContextualMenuItem[] = [];
const isNotebookUntrusted = this.props.isNotebookUntrusted;
const runTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
if (this.props.cellType === "code") { if (this.props.cellType === "code") {
items = items.concat([ items = items.concat([
{ {
key: "Run", key: "Run",
text: "Run", text: "Run",
title: runTooltip,
disabled: isNotebookUntrusted,
onClick: () => { onClick: () => {
this.props.executeCell(); this.props.executeCell();
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark); this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
@@ -223,6 +228,7 @@ const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state
cellIdAbove, cellIdAbove,
cellIdBelow, cellIdBelow,
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell), hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, ownProps.contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PromptContent renders for busy status 1`] = `
<div
className="greyStopButton"
style={
Object {
"left": 0,
"maxHeight": "100%",
"position": "sticky",
"top": 0,
"width": "100%",
"zIndex": 300,
}
}
>
<CustomizedIconButton
ariaLabel="Stop cell execution"
className="runCellButton"
iconProps={
Object {
"iconName": "CircleStopSolid",
}
}
style={
Object {
"position": "absolute",
}
}
title="Stop cell execution"
/>
<StyledSpinnerBase
size={3}
style={
Object {
"paddingTop": 5,
"position": "absolute",
"width": "100%",
}
}
/>
</div>
`;
exports[`PromptContent renders when hovered 1`] = `
<div
className=""
>
<CustomizedIconButton
ariaLabel="Run cell"
className="runCellButton"
iconProps={
Object {
"iconName": "MSNVideosSolid",
}
}
title="Run cell"
/>
</div>
`;

View File

@@ -11,7 +11,6 @@ import {
DropTargetConnector, DropTargetConnector,
DropTargetMonitor, DropTargetMonitor,
} from "react-dnd"; } from "react-dnd";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import styled, { StyledComponent } from "styled-components"; import styled, { StyledComponent } from "styled-components";
@@ -123,9 +122,10 @@ export const cellTarget = {
drop(props: Props, monitor: DropTargetMonitor, component: any): void { drop(props: Props, monitor: DropTargetMonitor, component: any): void {
if (monitor) { if (monitor) {
const hoverUpperHalf = isDragUpper(props, monitor, component.el); const hoverUpperHalf = isDragUpper(props, monitor, component.el);
const item: Props = monitor.getItem();
// DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation. // DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation.
props.moveCell({ props.moveCell({
id: monitor.getItem().id, id: item.id,
destinationId: props.id, destinationId: props.id,
above: hoverUpperHalf, above: hoverUpperHalf,
contentRef: props.contentRef, contentRef: props.contentRef,

View File

@@ -4,6 +4,7 @@ import Immutable from "immutable";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { NotebookUtil } from "../../../NotebookUtil";
interface ComponentProps { interface ComponentProps {
contentRef: ContentRef; contentRef: ContentRef;
@@ -14,6 +15,7 @@ interface StateProps {
cellMap: Immutable.Map<string, any>; cellMap: Immutable.Map<string, any>;
cellOrder: Immutable.List<string>; cellOrder: Immutable.List<string>;
focusedCell?: string | null; focusedCell?: string | null;
isNotebookUntrusted: boolean;
} }
interface DispatchProps { interface DispatchProps {
@@ -59,8 +61,13 @@ export class KeyboardShortcuts extends React.Component<Props> {
cellOrder, cellOrder,
focusedCell, focusedCell,
cellMap, cellMap,
isNotebookUntrusted,
} = this.props; } = this.props;
if (isNotebookUntrusted) {
return;
}
let ctrlKeyPressed = e.ctrlKey; let ctrlKeyPressed = e.ctrlKey;
// Allow cmd + enter (macOS) to operate like ctrl + enter // Allow cmd + enter (macOS) to operate like ctrl + enter
if (process.platform === "darwin") { if (process.platform === "darwin") {
@@ -125,6 +132,7 @@ export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps)
cellOrder, cellOrder,
cellMap, cellMap,
focusedCell, focusedCell,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
}; };
}; };
return mapStateToProps; return mapStateToProps;

View File

@@ -1,4 +1,5 @@
import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable"; import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable";
import { AppState, selectors } from "@nteract/core";
import domtoimage from "dom-to-image"; import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas"; import Html2Canvas from "html2canvas";
import path from "path"; import path from "path";
@@ -11,6 +12,8 @@ import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentI
export type FileType = "directory" | "file" | "notebook"; export type FileType = "directory" | "file" | "notebook";
// Utilities for notebooks // Utilities for notebooks
export class NotebookUtil { export class NotebookUtil {
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
/** /**
* It's a notebook file if the filename ends with .ipynb. * It's a notebook file if the filename ends with .ipynb.
*/ */
@@ -153,6 +156,16 @@ export class NotebookUtil {
); );
} }
public static isNotebookUntrusted(state: AppState, contentRef: string): boolean {
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const metadata = selectors.notebook.metadata(content.model);
return metadata.getIn(["untrusted"]) as boolean;
}
return false;
}
/** /**
* Find code cells with display * Find code cells with display
* @param notebookObject * @param notebookObject

View File

@@ -0,0 +1,31 @@
import { shallow } from "enzyme";
import React from "react";
import { SecurityWarningBar } from "./SecurityWarningBar";
describe("SecurityWarningBar", () => {
it("renders if notebook is untrusted", () => {
const wrapper = shallow(
<SecurityWarningBar
contentRef={"contentRef"}
isNotebookUntrusted={true}
markNotebookAsTrusted={undefined}
saveNotebook={undefined}
/>
);
expect(wrapper).toMatchSnapshot();
});
it("renders if notebook is trusted", () => {
const wrapper = shallow(
<SecurityWarningBar
contentRef={"contentRef"}
isNotebookUntrusted={false}
markNotebookAsTrusted={undefined}
saveNotebook={undefined}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,93 @@
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { actions, AppState } from "@nteract/core";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { NotebookUtil } from "../NotebookUtil";
export interface SecurityWarningBarPureProps {
contentRef: string;
}
interface SecurityWarningBarDispatchProps {
markNotebookAsTrusted: (contentRef: string) => void;
saveNotebook: (contentRef: string) => void;
}
type SecurityWarningBarProps = SecurityWarningBarPureProps & StateProps & SecurityWarningBarDispatchProps;
interface SecurityWarningBarState {
isBarDismissed: boolean;
}
export class SecurityWarningBar extends React.Component<SecurityWarningBarProps, SecurityWarningBarState> {
constructor(props: SecurityWarningBarProps) {
super(props);
this.state = {
isBarDismissed: false,
};
}
render(): JSX.Element {
return this.props.isNotebookUntrusted && !this.state.isBarDismissed ? (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={false}
onDismiss={() => this.setState({ isBarDismissed: true })}
dismissButtonAriaLabel="Close"
actions={
<MessageBarButton
onClick={() => {
this.props.markNotebookAsTrusted(this.props.contentRef);
this.props.saveNotebook(this.props.contentRef);
}}
>
Trust Notebook
</MessageBarButton>
}
>
{" "}
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone
else may involve security risks.
</MessageBar>
) : (
<></>
);
}
}
interface StateProps {
isNotebookUntrusted: boolean;
}
interface InitialProps {
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const mapStateToProps = (state: AppState): StateProps => ({
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, initialProps.contentRef),
});
return mapStateToProps;
};
const makeMapDispatchToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch): SecurityWarningBarDispatchProps => {
return {
markNotebookAsTrusted: (contentRef: string) => {
return dispatch(
actions.deleteMetadataField({
contentRef,
field: "untrusted",
})
);
},
saveNotebook: (contentRef: string) => dispatch(actions.save({ contentRef })),
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SecurityWarningBar);

View File

@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SecurityWarningBar renders if notebook is trusted 1`] = `<Fragment />`;
exports[`SecurityWarningBar renders if notebook is untrusted 1`] = `
<StyledMessageBar
actions={
<CustomizedMessageBarButton
onClick={[Function]}
>
Trust Notebook
</CustomizedMessageBarButton>
}
dismissButtonAriaLabel="Close"
isMultiline={false}
messageBarType={5}
onDismiss={[Function]}
>
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone else may involve security risks.
</StyledMessageBar>
`;

View File

@@ -1,3 +1,4 @@
import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@@ -5,8 +6,14 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
interface NotebookState { interface NotebookState {
isNotebookEnabled: boolean; isNotebookEnabled: boolean;
@@ -18,6 +25,9 @@ interface NotebookState {
isShellEnabled: boolean; isShellEnabled: boolean;
notebookBasePath: string; notebookBasePath: string;
isInitializingNotebooks: boolean; isInitializingNotebooks: boolean;
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -27,9 +37,15 @@ interface NotebookState {
setIsShellEnabled: (isShellEnabled: boolean) => void; setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void; setNotebookBasePath: (notebookBasePath: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>; refreshNotebooksEnabledStateForAccount: () => Promise<void>;
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set) => ({ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isNotebookEnabled: false, isNotebookEnabled: false,
isNotebooksEnabledForAccount: false, isNotebooksEnabledForAccount: false,
notebookServerInfo: { notebookServerInfo: {
@@ -46,6 +62,9 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
isShellEnabled: false, isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath, notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false, isInitializingNotebooks: false,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -103,4 +122,128 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
set({ isNotebooksEnabledForAccount: false }); set({ isNotebooksEnabledForAccount: false });
} }
}, },
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
const currentItem = root || get().myNotebooksContentRoot;
if (currentItem) {
if (currentItem.path === item.path && currentItem.name === item.name) {
return currentItem;
}
if (currentItem.children) {
for (const childItem of currentItem.children) {
const result = get().findItem(childItem, item);
if (result) {
return result;
}
}
}
}
return undefined;
},
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, parent);
item.parent = parentItem;
if (parentItem.children) {
parentItem.children.push(item);
} else {
parentItem.children = [item];
}
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item);
item.parent = parentItem;
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const myNotebooksContentRoot = {
name: "My Notebooks",
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos",
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
}
: undefined;
set({
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
});
if (get().notebookServerInfo?.notebookServerEndpoint) {
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
set({ myNotebooksContentRoot: updatedRoot });
if (updatedRoot?.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
updatedRoot.children.forEach((notebookItem) => {
switch (notebookItem.type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
}
},
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
if (gitHubNotebooksContentRoot) {
gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
children: [],
parent: gitHubNotebooksContentRoot,
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
parent: repoTreeItem,
});
});
gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
set({ gitHubNotebooksContentRoot });
}
},
})); }));

View File

@@ -113,7 +113,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: "", collectionId: "",
enableIndexing: true, enableIndexing: true,
isSharded: userContext.apiType !== "Tables", isSharded: userContext.apiType !== "Tables",
partitionKey: "", partitionKey: this.getPartitionKey(),
enableDedicatedThroughput: false, enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"), createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
useHashV2: false, useHashV2: false,
@@ -413,6 +413,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</TooltipHost> </TooltipHost>
</Stack> </Stack>
<Text variant="small" aria-label="pkDescription">
{this.getPartitionKeySubtext()}
</Text>
<input <input
type="text" type="text"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
@@ -807,6 +811,30 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return tooltipText; return tooltipText;
} }
private getPartitionKey(): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
return "";
}
private getPartitionKeySubtext(): string {
if (
userContext.features.partitionKeyDefault &&
(userContext.apiType === "SQL" || userContext.apiType === "Mongo")
) {
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
return subtext;
}
return "";
}
private getAnalyticalStorageTooltipContent(): JSX.Element { private getAnalyticalStorageTooltipContent(): JSX.Element {
return ( return (
<Text variant="small"> <Text variant="small">

View File

@@ -98,6 +98,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => { const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem; let parent: NotebookContentItem;
let isGithubTree: boolean;
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
@@ -105,21 +106,23 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
path: useNotebook.getState().notebookBasePath, path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
isGithubTree = false;
break; break;
case "GitHub": case "GitHub":
parent = { parent = {
name: ResourceTreeAdapter.GitHubReposTitle, name: selectedLocation.branch,
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
isGithubTree = true;
break; break;
default: default:
throw new Error(`Unsupported location type ${location.type}`); throw new Error(`Unsupported location type ${location.type}`);
} }
return container.uploadFile(name, content, parent); return container.uploadFile(name, content, parent, isGithubTree);
}; };
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => { const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {

View File

@@ -23,7 +23,7 @@ import { PanelLoadingScreen } from "../PanelLoadingScreen";
interface IGitHubReposPanelProps { interface IGitHubReposPanelProps {
explorer: Explorer; explorer: Explorer;
gitHubClientProp: GitHubClient; gitHubClientProp: GitHubClient;
junoClientProp?: JunoClient; junoClientProp: JunoClient;
} }
interface IGitHubReposPanelState { interface IGitHubReposPanelState {
@@ -120,6 +120,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos"); handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
} }
} }
useSidePanel.getState().closeSidePanel();
} }
public resetData(): void { public resetData(): void {
@@ -144,11 +145,18 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
private setup(forceShowConnectToGitHub = false): void { private setup(forceShowConnectToGitHub = false): void {
forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn() forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn()
? this.setupForConnectToGitHub() ? this.setupForConnectToGitHub(forceShowConnectToGitHub)
: this.setupForManageRepos(); : this.setupForManageRepos();
} }
private setupForConnectToGitHub(): void { private setupForConnectToGitHub(forceShowConnectToGitHub: boolean): void {
if (forceShowConnectToGitHub) {
const newState = { ...this.state.gitHubReposState };
newState.showAuthorizeAccess = forceShowConnectToGitHub;
this.setState({
gitHubReposState: newState,
});
}
this.setState({ this.setState({
isExecuting: false, isExecuting: false,
}); });
@@ -368,14 +376,17 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
isLoading: true, isLoading: true,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo), loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
}; };
this.loadMoreBranches(item.repo);
}
});
this.setState({ this.setState({
gitHubReposState: { gitHubReposState: {
...this.state.gitHubReposState, ...this.state.gitHubReposState,
reposListProps: { reposListProps: {
...this.state.gitHubReposState.reposListProps, ...this.state.gitHubReposState.reposListProps,
branchesProps: { branchesProps: {
...this.state.gitHubReposState.reposListProps.branchesProps, ...this.branchesProps,
[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]: this.branchesProps[item.key],
}, },
pinnedReposProps: { pinnedReposProps: {
repos: this.pinnedReposProps.repos, repos: this.pinnedReposProps.repos,
@@ -387,27 +398,6 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
}, },
}, },
}); });
this.loadMoreBranches(item.repo);
} else {
if (this.isAddedRepo === false) {
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
}
}
});
this.isAddedRepo = false; this.isAddedRepo = false;
} }

View File

@@ -33,10 +33,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
}, },
"getRepo": [Function], "getRepo": [Function],
"pinRepo": [Function], "pinRepo": [Function],

View File

@@ -150,9 +150,6 @@
.backImageIcon { .backImageIcon {
margin-top: 8px; margin-top: 8px;
} }
.entityValueTextField {
margin: 24px;
}
.addEntityDatePicker { .addEntityDatePicker {
max-width: 145px; max-width: 145px;
} }

View File

@@ -4,8 +4,8 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
export interface PanelContainerProps { export interface PanelContainerProps {
headerText: string; headerText?: string;
panelContent: JSX.Element; panelContent?: JSX.Element;
isConsoleExpanded: boolean; isConsoleExpanded: boolean;
isOpen: boolean; isOpen: boolean;
panelWidth?: string; panelWidth?: string;
@@ -66,8 +66,8 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
); );
} }
private onDissmiss = (ev?: React.SyntheticEvent<HTMLElement>): void => { private onDissmiss = (ev?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
if ((ev.target as HTMLElement).id === "notificationConsoleHeader") { if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
ev.preventDefault(); ev.preventDefault();
} else { } else {
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
@@ -85,11 +85,12 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
export const SidePanel: React.FC = () => { export const SidePanel: React.FC = () => {
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded); const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
const { isOpen, panelContent, headerText } = useSidePanel((state) => { const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
return { return {
isOpen: state.isOpen, isOpen: state.isOpen,
panelContent: state.panelContent, panelContent: state.panelContent,
headerText: state.headerText, headerText: state.headerText,
panelWidth: state.panelWidth,
}; };
}); });
// TODO Refactor PanelContainerComponent into a functional component and remove this wrapper // TODO Refactor PanelContainerComponent into a functional component and remove this wrapper
@@ -100,6 +101,7 @@ export const SidePanel: React.FC = () => {
panelContent={panelContent} panelContent={panelContent}
headerText={headerText} headerText={headerText}
isConsoleExpanded={isConsoleExpanded} isConsoleExpanded={isConsoleExpanded}
panelWidth={panelWidth}
/> />
); );
}; };

View File

@@ -105,7 +105,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
notebookName, notebookName,
notebookDescription, notebookDescription,
notebookTags?.split(","), notebookTags?.split(","),
author,
imageSrc, imageSrc,
content content
); );

View File

@@ -23,10 +23,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"copyNotebook": [Function], "copyNotebook": [Function],
"parameters": [Function], "parameters": [Function],
}, },
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
} }
} }
inProgressMessage="Creating directory " inProgressMessage="Creating directory "

View File

@@ -1,11 +1,11 @@
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react"; import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg"; import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity"; import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants"; import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
@@ -15,7 +15,7 @@ import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../.
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities"; import * as Utilities from "../../Tables/Utilities";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { PanelContainerComponent } from "../PanelContainerComponent"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@@ -30,9 +30,7 @@ import {
getCassandraDefaultEntities, getCassandraDefaultEntities,
getDefaultEntities, getDefaultEntities,
getEntityValuePlaceholder, getEntityValuePlaceholder,
getPanelTitle,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
@@ -61,7 +59,6 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
tableEntityListViewModel, tableEntityListViewModel,
cassandraApiClient, cassandraApiClient,
}: AddTableEntityPanelProps): JSX.Element => { }: AddTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]); const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0); const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>(""); const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
@@ -70,6 +67,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
isEntityValuePanelOpen, isEntityValuePanelOpen,
{ setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse }, { setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse },
] = useBoolean(false); ] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
/* Get default and previous saved entity headers */ /* Get default and previous saved entity headers */
useEffect(() => { useEffect(() => {
@@ -98,19 +97,36 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
}; };
/* Add new entity attribute */ /* Add new entity attribute */
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => { const onSubmit = async (): Promise<void> => {
if (!isValidEntities(entities)) { for (let i = 0; i < entities.length; i++) {
return undefined; const { property, type } = entities[i];
if (property === "" || property === undefined) {
setFormError(`Property name cannot be empty. Please enter a property name`);
return;
} }
event.preventDefault();
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
}
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities); const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try {
await tableEntityListViewModel.addEntityToCache(newEntity); await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled(); tableEntityListViewModel.redrawTableThrottled();
} }
closeSidePanel(); } catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
handleError(errorMessage, "AddTableRow");
throw error;
} finally {
setIsExecuting(false);
}
}; };
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -200,10 +216,35 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setIsEntityValuePanelTrue(); setIsEntityValuePanelTrue();
}; };
const renderPanelContent = (): JSX.Element => { if (isEntityValuePanelOpen) {
return ( return (
<form className="panelFormWrapper"> <Stack style={{ padding: "20px 34px" }}>
<div className="panelFormWrapper"> <Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
<TextField
multiline
rows={5}
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value");
setEntityAttributeValue(newInput);
}}
/>
</Stack>
);
}
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: getButtonLabel(userContext.apiType),
onSubmit,
};
return (
<RightPaneForm {...props}>
<div className="panelMainContent"> <div className="panelMainContent">
{entities.map((entity, index) => { {entities.map((entity, index) => {
return ( return (
@@ -249,61 +290,6 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
</Stack> </Stack>
)} )}
</div> </div>
<div className="paneFooter"> </RightPaneForm>
<div className="leftpanel-okbut">
<input
type="submit"
onClick={submit}
className="genericPaneSubmitBtn"
value={getButtonLabel(userContext.apiType)}
/>
</div>
</div>
</div>
</form>
);
};
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => {
return (
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
);
};
if (isEntityValuePanelOpen) {
return (
<PanelContainerComponent
headerText=""
onRenderNavigationContent={onRenderNavigationContent}
panelWidth="700px"
isOpen={true}
panelContent={
<TextField
multiline
rows={5}
className="entityValueTextField"
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value");
setEntityAttributeValue(newInput);
}}
/>
}
isConsoleExpanded={false}
/>
);
}
return (
<PanelContainerComponent
headerText={getPanelTitle(userContext.apiType)}
panelWidth="700px"
isOpen={true}
panelContent={renderPanelContent()}
isConsoleExpanded={false}
/>
); );
}; };

View File

@@ -1,11 +1,11 @@
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react"; import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg"; import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity"; import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants"; import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities"; import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
@@ -14,7 +14,7 @@ import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { PanelContainerComponent } from "../PanelContainerComponent"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@@ -29,7 +29,6 @@ import {
getEntityValuePlaceholder, getEntityValuePlaceholder,
getFormattedTime, getFormattedTime,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
@@ -59,12 +58,13 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
tableEntityListViewModel, tableEntityListViewModel,
cassandraApiClient, cassandraApiClient,
}: EditTableEntityPanelProps): JSX.Element => { }: EditTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]); const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0); const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>(""); const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({}); const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({});
const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>(""); const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [ const [
isEntityValuePanelOpen, isEntityValuePanelOpen,
@@ -190,14 +190,26 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
return displayValue; return displayValue;
}; };
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => { const onSubmit = async (): Promise<void> => {
if (!isValidEntities(entities)) { for (let i = 0; i < entities.length; i++) {
return undefined; const { property, type } = entities[i];
if (property === "" || property === undefined) {
setFormError(`Property name cannot be empty. Please enter a property name`);
return;
} }
event.preventDefault();
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
}
setIsExecuting(true);
const entity: Entities.ITableEntity = entityFromAttributes(entities); const entity: Entities.ITableEntity = entityFromAttributes(entities);
const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient; const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient;
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument; const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
try {
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument( const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
queryTablesTab.collection, queryTablesTab.collection,
originalDocumentData, originalDocumentData,
@@ -209,7 +221,13 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
} }
tableEntityListViewModel.selected.removeAll(); tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity); tableEntityListViewModel.selected.push(newEntity);
closeSidePanel(); } catch (error) {
const errorMessage = getErrorMessage(error);
handleError(errorMessage, "EditTableRow");
throw error;
} finally {
setIsExecuting(false);
}
}; };
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
@@ -299,10 +317,35 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
setIsEntityValuePanelTrue(); setIsEntityValuePanelTrue();
}; };
const renderPanelContent = (): JSX.Element => { if (isEntityValuePanelOpen) {
return ( return (
<form className="panelFormWrapper"> <Stack style={{ padding: "20px 34px" }}>
<div className="panelFormWrapper"> <Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
<TextField
multiline
rows={5}
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
setEntityAttributeValue(newInput);
entityChange(newInput, selectedRow, "value");
}}
/>
</Stack>
);
}
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: "Update",
onSubmit,
};
return (
<RightPaneForm {...props}>
<div className="panelMainContent"> <div className="panelMainContent">
{entities.map((entity, index) => { {entities.map((entity, index) => {
return ( return (
@@ -349,59 +392,6 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
</Stack> </Stack>
)} )}
</div> </div>
{renderPanelFooter()} </RightPaneForm>
</div>
</form>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<input type="submit" onClick={submit} className="genericPaneSubmitBtn" value="Update Entity" />
</div>
</div>
);
};
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => (
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
);
if (isEntityValuePanelOpen) {
return (
<PanelContainerComponent
headerText=""
onRenderNavigationContent={onRenderNavigationContent}
panelWidth="700px"
isOpen={true}
panelContent={
<TextField
multiline
rows={5}
className="entityValueTextField"
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
setEntityAttributeValue(newInput);
entityChange(newInput, selectedRow, "value");
}}
/>
}
isConsoleExpanded={false}
/>
);
}
return (
<PanelContainerComponent
headerText="Edit Table Entity"
panelWidth="700px"
isOpen={true}
panelContent={renderPanelContent()}
isConsoleExpanded={false}
/>
); );
}; };

View File

@@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2
export const columnProps: Partial<IStackProps> = { export const columnProps: Partial<IStackProps> = {
tokens: { childrenGap: 10 }, tokens: { childrenGap: 10 },
styles: { root: { width: 680 } }, styles: { root: { marginBottom: 8 } },
}; };
// helper functions // helper functions
@@ -134,8 +134,8 @@ export const getEntityValuePlaceholder = (entityType: string | number): string =
export const isValidEntities = (entities: EntityRowType[]): boolean => { export const isValidEntities = (entities: EntityRowType[]): boolean => {
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
const { property } = entities[i]; const { property, type } = entities[i];
if (property === "" || property === undefined) { if (property === "" || property === undefined || !type) {
return false; return false;
} }
} }
@@ -170,13 +170,6 @@ export const getDefaultEntities = (headers: string[], entityTypes: EntityType):
return defaultEntities; return defaultEntities;
}; };
export const getPanelTitle = (apiType: string): string => {
if (apiType === "Cassandra") {
return "Add Table Row";
}
return "Add Table Row";
};
export const getAddButtonLabel = (apiType: string): string => { export const getAddButtonLabel = (apiType: string): string => {
if (apiType === "Cassandra") { if (apiType === "Cassandra") {
return "Add Row"; return "Add Row";

View File

@@ -91,9 +91,7 @@ export const UploadItemsPane: FunctionComponent = () => {
accept="application/json" accept="application/json"
multiple multiple
tabIndex={0} tabIndex={0}
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
documents. The combined size of all files in an individual upload operation must be less than 2 MB. You
can perform multiple upload operations for larger data sets."
/> />
{uploadFileData?.length > 0 && ( {uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer"> <div className="fileUploadSummaryContainer">

View File

@@ -83,7 +83,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public render(): JSX.Element { public render(): JSX.Element {
const mainItems = this.createMainItems(); const mainItems = this.createMainItems();
const commonTaskItems = this.createCommonTaskItems(); const commonTaskItems = this.createCommonTaskItems();
const recentItems = this.createRecentItems(); let recentItems = this.createRecentItems();
if (userContext.features.notebooksTemporarilyDown) {
recentItems = recentItems.filter((item) => item.description !== "Notebook");
}
const tipsItems = this.createTipsItems(); const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent; const onClearRecent = this.clearMostRecent;
@@ -219,7 +223,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}); });
} }
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) {
heroes.push({ heroes.push({
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
title: "New Notebook", title: "New Notebook",

View File

@@ -1,4 +1,5 @@
export function getQuotedCqlIdentifier(identifier: string): string { // Added return type optional undefined because passing undefined from test cases.
export function getQuotedCqlIdentifier(identifier: string | undefined): string | undefined {
let result = identifier; let result = identifier;
if (!identifier) { if (!identifier) {
return result; return result;

View File

@@ -1,5 +1,6 @@
import Q from "q"; import Q from "q";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { useDialog } from "../../Controls/Dialog";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import * as DataTableUtilities from "./DataTableUtilities"; import * as DataTableUtilities from "./DataTableUtilities";
@@ -69,11 +70,16 @@ export default class TableCommands {
return null; // Error return null; // Error
} }
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
let deleteMessage: string = "Are you sure you want to delete the selected entities?"; const deleteMessage: string =
if (userContext.apiType === "Cassandra") { userContext.apiType === "Cassandra"
deleteMessage = "Are you sure you want to delete the selected rows?"; ? "Are you sure you want to delete the selected rows?"
} : "Are you sure you want to delete the selected entities?";
if (window.confirm(deleteMessage)) {
useDialog.getState().showOkCancelModalDialog(
"Confirm delete",
deleteMessage,
"Delete",
() => {
viewModel.queryTablesTab.container.tableDataClient viewModel.queryTablesTab.container.tableDataClient
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
.then((results: any) => { .then((results: any) => {
@@ -81,7 +87,11 @@ export default class TableCommands {
viewModel.redrawTableThrottled(); viewModel.redrawTableThrottled();
}); });
}); });
} },
"Cancel",
undefined
);
return null; return null;
} }

View File

@@ -21,6 +21,7 @@ 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 TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import ConflictId from "../Tree/ConflictId"; import ConflictId from "../Tree/ConflictId";
@@ -228,7 +229,7 @@ export default class ConflictsTab extends TabsBase {
this._documentsIterator = this.createIterator(); this._documentsIterator = this.createIterator();
await this.loadNextPage(); await this.loadNextPage();
} catch (error) { } catch (error) {
window.alert(getErrorMessage(error)); useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
} }
} }
@@ -252,10 +253,23 @@ export default class ConflictsTab extends TabsBase {
} }
public onAcceptChangesClick = async (): Promise<void> => { public onAcceptChangesClick = async (): Promise<void> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { if (this.isEditorDirty()) {
return; useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
async () => await this.resolveConflict(),
"Cancel",
undefined
);
} else {
await this.resolveConflict();
} }
};
private resolveConflict = async (): Promise<void> => {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@@ -318,7 +332,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) { } catch (error) {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.ResolveConflict, Action.ResolveConflict,
{ {
@@ -372,7 +386,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) { } catch (error) {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteConflict, Action.DeleteConflict,
{ {
@@ -662,11 +676,6 @@ export default class ConflictsTab extends TabsBase {
return jsonObject; return jsonObject;
} }
private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg);
};
private _getPartitionKeyPropertyHeader(): string { private _getPartitionKeyPropertyHeader(): string {
return ( return (
(this.partitionKey && (this.partitionKey &&

View File

@@ -25,6 +25,7 @@ import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils"; import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
@@ -378,7 +379,7 @@ export default class DocumentsTab extends TabsBase {
this.isFilterExpanded(false); this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus(); document.getElementById("errorStatusIcon")?.focus();
} catch (error) { } catch (error) {
window.alert(getErrorMessage(error)); useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
} }
} }
@@ -401,18 +402,29 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
} }
public onNewDocumentClick = (): Q.Promise<any> => { public onNewDocumentClick = (): void => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { if (this.isEditorDirty()) {
return Q(); useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
() => this.initializeNewDocument(),
"Cancel",
undefined
);
} else {
this.initializeNewDocument();
} }
this.selectedDocumentId(null); };
private initializeNewDocument = (): void => {
this.selectedDocumentId(null);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
this.initialDocumentContent(defaultDocument); this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
return Q();
}; };
public onSaveNewDocumentClick = (): Promise<any> => { public onSaveNewDocumentClick = (): Promise<any> => {
@@ -453,7 +465,7 @@ export default class DocumentsTab extends TabsBase {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateDocument, Action.CreateDocument,
{ {
@@ -516,7 +528,7 @@ export default class DocumentsTab extends TabsBase {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.UpdateDocument, Action.UpdateDocument,
{ {
@@ -546,9 +558,16 @@ export default class DocumentsTab extends TabsBase {
? "Are you sure you want to delete the selected item ?" ? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?"; : "Are you sure you want to delete the selected document ?";
if (window.confirm(msg)) { useDialog
await this._deleteDocument(selectedDocumentId); .getState()
} .showOkCancelModalDialog(
"Confirm delete",
msg,
"Delete",
async () => await this._deleteDocument(selectedDocumentId),
"Cancel",
undefined
);
}; };
public onValidDocumentEdit(): Q.Promise<any> { public onValidDocumentEdit(): Q.Promise<any> {
@@ -617,11 +636,6 @@ export default class DocumentsTab extends TabsBase {
} }
} }
private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg);
};
protected __deleteDocument(documentId: DocumentId): Promise<void> { protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection, documentId); return deleteDocument(this.collection, documentId);
} }

View File

@@ -16,6 +16,7 @@ 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 TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Controls/Dialog";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
import ObjectId from "../Tree/ObjectId"; import ObjectId from "../Tree/ObjectId";
import DocumentsTab from "./DocumentsTab"; import DocumentsTab from "./DocumentsTab";
@@ -111,7 +112,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateDocument, Action.CreateDocument,
{ {
@@ -169,7 +170,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
(error) => { (error) => {
this.isExecutionError(true); this.isExecutionError(true);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
window.alert(errorMessage); useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.UpdateDocument, Action.UpdateDocument,
{ {

View File

@@ -17,12 +17,14 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import * as CdbActions from "../Notebook/NotebookComponent/actions"; import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
@@ -59,7 +61,9 @@ export default class NotebookTabV2 extends NotebookTabBase {
}; };
if (this.notebookComponentAdapter.isContentDirty()) { if (this.notebookComponentAdapter.isContentDirty()) {
this.container.showOkCancelModalDialog( useDialog
.getState()
.showOkCancelModalDialog(
"Close without saving?", "Close without saving?",
`File has unsaved changes, close without saving?`, `File has unsaved changes, close without saving?`,
"Close", "Close",
@@ -84,11 +88,13 @@ export default class NotebookTabV2 extends NotebookTabBase {
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted();
const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
const saveLabel = "Save"; const saveLabel = "Save";
const copyToLabel = "Copy to ..."; const copyToLabel = "Copy to ...";
const publishLabel = "Publish to gallery"; const publishLabel = "Publish to gallery";
const workspaceLabel = "No Workspace";
const kernelLabel = "No Kernel"; const kernelLabel = "No Kernel";
const runLabel = "Run"; const runLabel = "Run";
const runActiveCellLabel = "Run Active Cell"; const runActiveCellLabel = "Run Active Cell";
@@ -105,8 +111,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
const copyLabel = "Copy"; const copyLabel = "Copy";
const cutLabel = "Cut"; const cutLabel = "Cut";
const pasteLabel = "Paste"; const pasteLabel = "Paste";
const undoLabel = "Undo";
const redoLabel = "Redo";
const cellCodeType = "code"; const cellCodeType = "code";
const cellMarkdownType = "markdown"; const cellMarkdownType = "markdown";
const cellRawType = "raw"; const cellRawType = "raw";
@@ -187,9 +191,10 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.traceTelemetry(Action.ExecuteCell); this.traceTelemetry(Action.ExecuteCell);
}, },
commandButtonLabel: runLabel, commandButtonLabel: runLabel,
tooltipText: runBtnTooltip,
ariaLabel: runLabel, ariaLabel: runLabel,
hasPopup: false, hasPopup: false,
disabled: false, disabled: isNotebookUntrusted,
children: [ children: [
{ {
iconSrc: RunIcon, iconSrc: RunIcon,

View File

@@ -137,13 +137,14 @@ export default class QueryTablesTab extends TabsBase {
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
"Add Table Entity", "Add Table Row",
<AddTableEntityPanel <AddTableEntityPanel
tableDataClient={this.tableDataClient} tableDataClient={this.tableDataClient}
queryTablesTab={this} queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()} tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()} cassandraApiClient={new CassandraAPIDataClient()}
/> />,
"700px"
); );
}; };
@@ -157,7 +158,8 @@ export default class QueryTablesTab extends TabsBase {
queryTablesTab={this} queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()} tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()} cassandraApiClient={new CassandraAPIDataClient()}
/> />,
"700px"
); );
}; };

View File

@@ -571,8 +571,8 @@ export default class Collection implements ViewModels.Collection {
}; };
public onSettingsClick = async (): Promise<void> => { public onSettingsClick = async (): Promise<void> => {
await this.loadOffer();
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
await this.loadOffer();
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node", description: "Settings node",
@@ -744,8 +744,8 @@ export default class Collection implements ViewModels.Collection {
StoredProcedure.create(source, event); StoredProcedure.create(source, event);
} }
public onNewUserDefinedFunctionClick(source: ViewModels.Collection, event: MouseEvent) { public onNewUserDefinedFunctionClick(source: ViewModels.Collection) {
UserDefinedFunction.create(source, event); UserDefinedFunction.create(source);
} }
public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) { public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) {

View File

@@ -1,12 +1,11 @@
import Q from "q"; import { extractPartitionKey } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import DocumentId from "./DocumentId";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { extractPartitionKey } from "@azure/cosmos";
import ConflictsTab from "../Tabs/ConflictsTab";
import { readDocument } from "../../Common/dataAccess/readDocument"; import { readDocument } from "../../Common/dataAccess/readDocument";
import * as DataModels from "../../Contracts/DataModels";
import { useDialog } from "../Controls/Dialog";
import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentId from "./DocumentId";
export default class ConflictId { export default class ConflictId {
public container: ConflictsTab; public container: ConflictsTab;
@@ -50,13 +49,20 @@ export default class ConflictId {
} }
public click() { public click() {
if ( if (this.container.isEditorDirty()) {
!this.container.isEditorDirty() || useDialog
window.confirm("Your unsaved changes will be lost. Do you want to continue?") .getState()
) { .showOkCancelModalDialog(
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
() => this.loadConflict(),
"Cancel",
undefined
);
} else {
this.loadConflict(); this.loadConflict();
} }
return;
} }
public async loadConflict(): Promise<void> { public async loadConflict(): Promise<void> {

View File

@@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database {
this.isOfferRead = false; this.isOfferRead = false;
} }
public onSettingsClick = () => { public onSettingsClick = (): void => {
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
@@ -193,6 +193,8 @@ export default class Database implements ViewModels.Database {
//merge collections //merge collections
this.addCollectionsToList(collectionVMs); this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete); this.deleteCollectionsFromList(deltaCollections.toDelete);
useDatabases.getState().updateDatabase(this);
} }
public async openAddCollection(database: Database): Promise<void> { public async openAddCollection(database: Database): Promise<void> {

View File

@@ -1,5 +1,6 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDialog } from "../Controls/Dialog";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
export default class DocumentId { export default class DocumentId {
@@ -28,10 +29,20 @@ export default class DocumentId {
} }
public click() { public click() {
if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) { if (this.container.isEditorDirty()) {
useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
() => this.loadDocument(),
"Cancel",
undefined
);
} else {
this.loadDocument(); this.loadDocument();
} }
return;
} }
public partitionKeyHeader(): Object { public partitionKeyHeader(): Object {

View File

@@ -1,45 +1,18 @@
import * as ko from "knockout"; import React from "react";
import * as React from "react";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
export class ResourceTreeAdapterForResourceToken implements ReactAdapter { export const ResourceTokenTree: React.FC = (): JSX.Element => {
public parameters: ko.Observable<number>; const collection = useDatabases((state) => state.resourceTokenCollection);
public myNotebooksContentRoot: NotebookContentItem;
public constructor(private container: Explorer) { const buildCollectionNode = (): TreeNode => {
this.parameters = ko.observable(Date.now());
useDatabases.subscribe(
() => this.triggerRender(),
(state) => state.resourceTokenCollection
);
useSelectedNode.subscribe(() => this.triggerRender());
useTabs.subscribe(
() => this.triggerRender(),
(state) => state.activeTab
);
this.triggerRender();
}
public renderComponent(): JSX.Element {
const dataRootNode = this.buildCollectionNode();
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
}
public buildCollectionNode(): TreeNode {
const collection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
if (!collection) { if (!collection) {
return { return {
label: undefined, label: undefined,
@@ -86,9 +59,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
isExpanded: true, isExpanded: true,
children: [collectionNode], children: [collectionNode],
}; };
} };
public triggerRender() { return <TreeComponent className="dataResourceTree" rootNode={buildCollectionNode()} />;
window.requestAnimationFrame(() => this.parameters(Date.now())); };
}
}

View File

@@ -0,0 +1,764 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as React from "react";
import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { Areas, Notebook } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
export const MyNotebooksTitle = "My Notebooks";
export const GitHubReposTitle = "GitHub repos";
interface ResourceTreeProps {
container: Explorer;
}
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
const databases = useDatabases((state) => state.databases);
const {
isNotebookEnabled,
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
updateNotebookItem,
} = useNotebook(
(state) => ({
isNotebookEnabled: state.isNotebookEnabled,
myNotebooksContentRoot: state.myNotebooksContentRoot,
galleryContentRoot: state.galleryContentRoot,
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
updateNotebookItem: state.updateNotebookItem,
}),
shallow
);
const { activeTab, refreshActiveTab } = useTabs();
const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
const pseudoDirPath = "PsuedoDir";
const buildGalleryCallout = (): JSX.Element => {
if (
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
) {
return undefined;
}
const calloutProps: ICalloutProps = {
calloutMaxWidth: 350,
ariaLabel: "New gallery",
role: "alertdialog",
gapSpace: 0,
target: ".galleryHeader",
directionalHint: DirectionalHint.leftTopEdge,
onDismiss: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
},
setInitialFocus: true,
};
const openGalleryProps: ILinkProps = {
onClick: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
container.openGallery();
},
};
return (
<Callout {...calloutProps}>
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
<Text variant="xLarge" block>
New gallery
</Text>
<Text block>
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
contributors.
</Text>
<Link {...openGalleryProps}>Open gallery</Link>
</Stack>
</Callout>
);
};
const buildNotebooksTree = (): TreeNode => {
const notebooksTree: TreeNode = {
label: undefined,
isExpanded: true,
children: [],
};
if (userContext.features.notebooksTemporarilyDown) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (myNotebooksContentRoot) {
notebooksTree.children.push(buildMyNotebooksTree());
}
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(buildGitHubNotebooksTree());
}
}
return notebooksTree;
};
const buildNotebooksTemporarilyDownTree = (): TreeNode => {
return {
label: Notebook.temporarilyDownMsg,
className: "clickDisabled",
};
};
const buildGalleryNotebooksTree = (): TreeNode => {
return {
label: "Gallery",
iconSrc: GalleryIcon,
className: "notebookHeader galleryHeader",
onClick: () => container.openGallery(),
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
};
};
const buildMyNotebooksTree = (): TreeNode => {
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
}
);
myNotebooksTree.isExpanded = true;
myNotebooksTree.isAlphaSorted = true;
// Remove "Delete" menu item from context menu
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
return myNotebooksTree;
};
const buildGitHubNotebooksTree = (): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
},
true
);
gitHubNotebooksTree.contextMenu = [
{
label: "Manage GitHub settings",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Manage GitHub settings",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
{
label: "Disconnect from GitHub",
onClick: () => {
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
container.notebookManager?.gitHubOAuthService.logout();
},
},
];
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;
return gitHubNotebooksTree;
};
const buildChildNodes = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode[] => {
if (!item || !item.children) {
return [];
} else {
return item.children.map((item) => {
const result =
item.type === NotebookContentItemType.Directory
? buildNotebookDirectoryNode(item, onFileClick, isGithubTree)
: buildNotebookFileNode(item, onFileClick, isGithubTree);
result.timestamp = item.timestamp;
return result;
});
}
};
const buildNotebookFileNode = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode => {
return {
label: item.name,
iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon,
className: "notebookHeader",
onClick: () => onFileClick(item),
isSelected: () => {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: createFileContextMenu(container, item, isGithubTree),
data: item,
};
};
const createFileContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => container.deleteNotebookFile(item, isGithubTree),
"Cancel",
undefined
);
},
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => copyNotebook(container, item),
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => container.downloadFile(item),
},
];
if (item.type === NotebookContentItemType.Notebook) {
items.push({
label: "Publish to gallery",
iconSrc: PublishIcon,
onClick: async () => {
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
source: Source.ResourceTreeMenu,
});
const content = await container.readFile(item);
if (content) {
await container.publishNotebook(item.name, content);
}
},
});
}
// "Copy to ..." isn't needed if github locations are not available
if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ...");
}
return items;
};
const copyNotebook = async (container: Explorer, item: NotebookContentItem) => {
const content = await container.readFile(item);
if (content) {
container.copyNotebook(item.name, content);
}
};
const createDirectoryContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [
{
label: "Refresh",
iconSrc: RefreshIcon,
onClick: () => loadSubitems(item, isGithubTree),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}?"`,
"Delete",
() => container.deleteNotebookFile(item, isGithubTree),
"Cancel",
undefined
);
},
},
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree),
},
{
label: "New Directory",
iconSrc: NewNotebookIcon,
onClick: () => container.onCreateDirectory(item, isGithubTree),
},
{
label: "New Notebook",
iconSrc: NewNotebookIcon,
onClick: () => container.onNewNotebookClicked(item, isGithubTree),
},
{
label: "Upload File",
iconSrc: NewNotebookIcon,
onClick: () => container.openUploadFilePanel(item),
},
];
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter(
(item) =>
item.label !== "Delete" &&
item.label !== "Rename" &&
item.label !== "New Directory" &&
item.label !== "Upload File"
);
}
return items;
};
const buildNotebookDirectoryNode = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean
): TreeNode => {
return {
label: item.name,
iconSrc: undefined,
className: "notebookHeader",
isAlphaSorted: true,
isLeavesParentsSeparate: true,
onClick: () => {
if (!item.children) {
loadSubitems(item, isGithubTree);
}
},
isSelected: () => {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined,
data: item,
children: buildChildNodes(item, onFileClick, isGithubTree),
};
};
const buildDataTree = (): TreeNode => {
const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => {
const databaseNode: TreeNode = {
label: database.id(),
iconSrc: CosmosDBIcon,
isExpanded: false,
className: "databaseHeader",
children: [],
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
onClick: async (isExpanded) => {
useSelectedNode.getState().setSelectedNode(database);
// Rewritten version of expandCollapseDatabase():
if (isExpanded) {
database.collapseDatabase();
} else {
if (databaseNode.children?.length === 0) {
databaseNode.isLoading = true;
}
await database.expandDatabase();
}
databaseNode.isLoading = false;
useCommandBar.getState().setContextButtons([]);
refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id());
},
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
};
if (database.isDatabaseShared()) {
databaseNode.children.push({
label: "Scale",
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettingsV2]),
onClick: database.onSettingsClick.bind(database),
});
}
// Find collections
database
.collections()
.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(buildCollectionNode(database, collection))
);
database.collections.subscribe((collections: ViewModels.Collection[]) => {
collections.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(buildCollectionNode(database, collection))
);
});
return databaseNode;
});
return {
label: undefined,
isExpanded: true,
children: databaseTreeNodes,
};
};
const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
const children: TreeNode[] = [];
children.push({
label: collection.getLabel(),
onClick: () => {
collection.openTab();
// push to most recent
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
},
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Documents,
ViewModels.CollectionTabKind.Graph,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
});
if (
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
!userContext.features.notebooksTemporarilyDown
) {
children.push({
label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
});
}
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
children.push({
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.CollectionSettingsV2,
]),
});
}
const schemaNode: TreeNode = buildSchemaNode(collection);
if (schemaNode) {
children.push(schemaNode);
}
if (showScriptNodes) {
children.push(buildStoredProcedureNode(collection));
children.push(buildUserDefinedFunctionsNode(collection));
children.push(buildTriggerNode(collection));
}
// This is a rewrite of showConflicts
const showConflicts =
userContext?.databaseAccount?.properties.enableMultipleWriteLocations &&
collection.rawDataModel &&
!!collection.rawDataModel.conflictResolutionPolicy;
if (showConflicts) {
children.push({
label: "Conflicts",
onClick: collection.onConflictsClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]),
});
}
return {
label: collection.id(),
iconSrc: CollectionIcon,
isExpanded: false,
children: children,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
onClick: () => {
// Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
onExpanded: () => {
if (showScriptNodes) {
collection.loadStoredProcedures();
collection.loadUserDefinedFunctions();
collection.loadTriggers();
}
},
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
};
};
const buildStoredProcedureNode = (collection: ViewModels.Collection): TreeNode => {
return {
label: "Stored Procedures",
children: collection.storedProcedures().map((sp: StoredProcedure) => ({
label: sp.id(),
onClick: sp.open.bind(sp),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.StoredProcedures,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp),
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
};
};
const buildUserDefinedFunctionsNode = (collection: ViewModels.Collection): TreeNode => {
return {
label: "User Defined Functions",
children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({
label: udf.id(),
onClick: udf.open.bind(udf),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.UserDefinedFunctions,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf),
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
};
};
const buildTriggerNode = (collection: ViewModels.Collection): TreeNode => {
return {
label: "Triggers",
children: collection.triggers().map((trigger: Trigger) => ({
label: trigger.id(),
onClick: trigger.open.bind(trigger),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger),
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
};
};
const buildSchemaNode = (collection: ViewModels.Collection): TreeNode => {
if (collection.analyticalStorageTtl() === undefined) {
return undefined;
}
if (!collection.schema || !collection.schema.fields) {
return undefined;
}
return {
label: "Schema",
children: getSchemaNodes(collection.schema.fields),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
refreshActiveTab((tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid);
},
};
};
const getSchemaNodes = (fields: DataModels.IDataField[]): TreeNode[] => {
const schema: any = {};
//unflatten
fields.forEach((field: DataModels.IDataField) => {
const path: string[] = field.path.split(".");
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
let current: any = {};
path.forEach((name: string, pathIndex: number) => {
if (pathIndex === 0) {
if (schema[name] === undefined) {
if (pathIndex === path.length - 1) {
schema[name] = fieldProperties;
} else {
schema[name] = {};
}
}
current = schema[name];
} else {
if (current[name] === undefined) {
if (pathIndex === path.length - 1) {
current[name] = fieldProperties;
} else {
current[name] = {};
}
}
current = current[name];
}
});
});
const traverse = (obj: any): TreeNode[] => {
const children: TreeNode[] = [];
if (obj !== undefined && !Array.isArray(obj) && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => {
children.push({ label: key, children: traverse(value) });
});
} else if (Array.isArray(obj)) {
return [{ label: obj[0] }, { label: obj[1] }];
}
return children;
};
return traverse(schema);
};
const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise<void> => {
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
updateNotebookItem(updatedItem, isGithubTree);
};
const dataRootNode = buildDataTree();
if (isNotebookEnabled) {
return (
<>
<AccordionComponent>
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
</AccordionItemComponent>
</AccordionComponent>
{buildGalleryCallout()}
</>
);
}
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
};

View File

@@ -27,6 +27,7 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
@@ -634,6 +635,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
<GitHubReposPanel <GitHubReposPanel
explorer={this.container} explorer={this.container}
gitHubClientProp={this.container.notebookManager.gitHubClient} gitHubClientProp={this.container.notebookManager.gitHubClient}
junoClientProp={this.container.notebookManager.junoClient}
/> />
), ),
}, },
@@ -711,7 +713,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
this.container.showOkCancelModalDialog( useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete", "Confirm delete",
`Are you sure you want to delete "${item.name}"`, `Are you sure you want to delete "${item.name}"`,
"Delete", "Delete",
@@ -776,7 +780,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Delete", label: "Delete",
iconSrc: DeleteIcon, iconSrc: DeleteIcon,
onClick: () => { onClick: () => {
this.container.showOkCancelModalDialog( useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete", "Confirm delete",
`Are you sure you want to delete "${item.name}?"`, `Are you sure you want to delete "${item.name}?"`,
"Delete", "Delete",

View File

@@ -1,35 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { TreeComponent, TreeComponentProps, TreeNode } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import ResourceTokenCollection from "./ResourceTokenCollection";
import { ResourceTreeAdapterForResourceToken } from "./ResourceTreeAdapterForResourceToken";
describe("Resource tree for resource token", () => {
const mockContainer = {} as Explorer;
const resourceTree = new ResourceTreeAdapterForResourceToken(mockContainer);
const mockCollection = {
_rid: "fakeRid",
_self: "fakeSelf",
id: "fakeId",
} as DataModels.Collection;
const mockResourceTokenCollection: ViewModels.CollectionBase = new ResourceTokenCollection(
mockContainer,
"fakeDatabaseId",
mockCollection
);
useDatabases.setState({ resourceTokenCollection: mockResourceTokenCollection });
it("should render", () => {
const rootNode: TreeNode = resourceTree.buildCollectionNode();
const props: TreeComponentProps = {
rootNode,
className: "dataResourceTree",
};
const wrapper = shallow(<TreeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -8,6 +8,7 @@ import { useTabs } from "../../hooks/useTabs";
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 { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { getErrorMessage } from "../Tables/Utilities"; import { getErrorMessage } from "../Tables/Utilities";
import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab"; import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab";
@@ -138,10 +139,11 @@ export default class StoredProcedure {
} }
}; };
public delete() { public delete() {
if (!window.confirm("Are you sure you want to delete the stored procedure?")) { useDialog.getState().showOkCancelModalDialog(
return; "Confirm delete",
} "Are you sure you want to delete the stored procedure?",
"Delete",
() => {
deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
@@ -149,6 +151,10 @@ export default class StoredProcedure {
}, },
(reason) => {} (reason) => {}
); );
},
"Cancel",
undefined
);
} }
public execute(params: string[], partitionKeyValue?: string): void { public execute(params: string[], partitionKeyValue?: string): void {

View File

@@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
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 { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import TriggerTab from "../Tabs/TriggerTab"; import TriggerTab from "../Tabs/TriggerTab";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -99,10 +100,11 @@ export default class Trigger {
}; };
public delete() { public delete() {
if (!window.confirm("Are you sure you want to delete the trigger?")) { useDialog.getState().showOkCancelModalDialog(
return; "Confirm delete",
} "Are you sure you want to delete the trigger?",
"Delete",
() => {
deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
@@ -110,5 +112,9 @@ export default class Trigger {
}, },
(reason) => {} (reason) => {}
); );
},
"Cancel",
undefined
);
} }
} }

View File

@@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
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 { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -30,7 +31,7 @@ export default class UserDefinedFunction {
this.body = ko.observable(data.body as string); this.body = ko.observable(data.body as string);
} }
public static create(source: ViewModels.Collection, event: MouseEvent) { public static create(source: ViewModels.Collection) {
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1; const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1;
const userDefinedFunction = { const userDefinedFunction = {
id: "", id: "",
@@ -95,16 +96,23 @@ export default class UserDefinedFunction {
} }
public delete() { public delete() {
if (!window.confirm("Are you sure you want to delete the user defined function?")) { useDialog.getState().showOkCancelModalDialog(
return; "Confirm delete",
} "Are you sure you want to delete the user defined function?",
"Delete",
() => {
deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
() => { () => {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);
}, },
(reason) => {} () => {
/**/
}
);
},
"Cancel",
undefined
); );
} }
} }

View File

@@ -1,36 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Resource tree for resource token should render 1`] = `
<div
className="treeComponent dataResourceTree"
role="tree"
>
<TreeNodeComponent
generation={0}
node={
Object {
"children": Array [
Object {
"children": Array [
Object {
"isSelected": [Function],
"label": "Items",
"onClick": [Function],
},
],
"className": "collectionHeader",
"iconSrc": "",
"isExpanded": true,
"isSelected": [Function],
"label": "fakeId",
"onClick": [Function],
},
],
"isExpanded": true,
"label": undefined,
}
}
paddingLeft={0}
/>
</div>
`;

View File

@@ -2,6 +2,7 @@ import _ from "underscore";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { useSelectedNode } from "./useSelectedNode"; import { useSelectedNode } from "./useSelectedNode";
interface DatabasesState { interface DatabasesState {
@@ -19,6 +20,8 @@ interface DatabasesState {
loadDatabaseOffers: () => Promise<void>; loadDatabaseOffers: () => Promise<void>;
isFirstResourceCreated: () => boolean; isFirstResourceCreated: () => boolean;
findSelectedDatabase: () => ViewModels.Database; findSelectedDatabase: () => ViewModels.Database;
validateDatabaseId: (id: string) => boolean;
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
} }
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
@@ -129,4 +132,17 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
return selectedNode.collection?.database; return selectedNode.collection?.database;
}, },
validateDatabaseId: (id: string): boolean => {
return !get().databases.some((database) => database.id() === id);
},
validateCollectionId: async (databaseId: string, collectionId: string): Promise<boolean> => {
const database = get().databases.find((db) => db.id() === databaseId);
// For a new tables account, database is undefined when creating the first table
if (!database && userContext.apiType === "Tables") {
return true;
}
await database.loadCollections();
return !database.collections().some((collection) => collection.id() === collectionId);
},
})); }));

View File

@@ -12,7 +12,7 @@ window.addEventListener("load", async () => {
if (openerWindow) { if (openerWindow) {
const params = new URLSearchParams(document.location.search); const params = new URLSearchParams(document.location.search);
await postRobot.send( await postRobot.send(
window, openerWindow,
GitHubConnectorMsgType, GitHubConnectorMsgType,
{ {
state: params.get("state"), state: params.get("state"),

View File

@@ -1,11 +1,13 @@
import { IContent } from "@nteract/core"; import { IContent } from "@nteract/core";
import { fixture } from "@nteract/fixtures"; import { fixture } from "@nteract/fixtures";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import * as GitHubUtils from "../Utils/GitHubUtils";
import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient"; import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient";
import { GitHubContentProvider } from "./GitHubContentProvider"; import { GitHubContentProvider } from "./GitHubContentProvider";
import * as GitHubUtils from "../Utils/GitHubUtils";
const gitHubClient = new GitHubClient(() => {}); const gitHubClient = new GitHubClient(() => {
/**/
});
const gitHubContentProvider = new GitHubContentProvider({ const gitHubContentProvider = new GitHubContentProvider({
gitHubClient, gitHubClient,
promptForCommitMsg: () => Promise.resolve("commit msg"), promptForCommitMsg: () => Promise.resolve("commit msg"),
@@ -46,7 +48,7 @@ const sampleNotebookModel: IContent<"notebook"> = {
created: "", created: "",
last_modified: "date", last_modified: "date",
mimetype: "application/x-ipynb+json", mimetype: "application/x-ipynb+json",
content: sampleFile.content ? JSON.parse(sampleFile.content) : null, content: sampleFile.content ? JSON.parse(sampleFile.content) : undefined,
format: "json", format: "json",
}; };
@@ -54,7 +56,7 @@ describe("GitHubContentProvider remove", () => {
it("errors on invalid path", async () => { it("errors on invalid path", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync"); spyOn(GitHubClient.prototype, "getContentsAsync");
const response = await gitHubContentProvider.remove(null, "invalid path").toPromise(); const response = await gitHubContentProvider.remove(undefined, "invalid path").toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
expect(gitHubClient.getContentsAsync).not.toBeCalled(); expect(gitHubClient.getContentsAsync).not.toBeCalled();
@@ -63,7 +65,7 @@ describe("GitHubContentProvider remove", () => {
it("errors on failed read", async () => { it("errors on failed read", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -75,7 +77,7 @@ describe("GitHubContentProvider remove", () => {
); );
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -90,7 +92,7 @@ describe("GitHubContentProvider remove", () => {
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
); );
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise(); const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(HttpStatusCodes.NoContent); expect(response.status).toBe(HttpStatusCodes.NoContent);
expect(gitHubClient.deleteFileAsync).toBeCalled(); expect(gitHubClient.deleteFileAsync).toBeCalled();
@@ -102,7 +104,7 @@ describe("GitHubContentProvider get", () => {
it("errors on invalid path", async () => { it("errors on invalid path", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync"); spyOn(GitHubClient.prototype, "getContentsAsync");
const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise(); const response = await gitHubContentProvider.get(undefined, "invalid path", undefined).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
expect(gitHubClient.getContentsAsync).not.toBeCalled(); expect(gitHubClient.getContentsAsync).not.toBeCalled();
@@ -111,7 +113,7 @@ describe("GitHubContentProvider get", () => {
it("errors on failed read", async () => { it("errors on failed read", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.get(null, sampleGitHubUri, null).toPromise(); const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, undefined).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -122,7 +124,7 @@ describe("GitHubContentProvider get", () => {
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
); );
const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise(); const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, {}).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -134,7 +136,7 @@ describe("GitHubContentProvider update", () => {
it("errors on invalid path", async () => { it("errors on invalid path", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync"); spyOn(GitHubClient.prototype, "getContentsAsync");
const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise(); const response = await gitHubContentProvider.update(undefined, "invalid path", undefined).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
expect(gitHubClient.getContentsAsync).not.toBeCalled(); expect(gitHubClient.getContentsAsync).not.toBeCalled();
@@ -143,7 +145,7 @@ describe("GitHubContentProvider update", () => {
it("errors on failed read", async () => { it("errors on failed read", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.update(null, sampleGitHubUri, null).toPromise(); const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, undefined).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -155,7 +157,7 @@ describe("GitHubContentProvider update", () => {
); );
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -170,7 +172,7 @@ describe("GitHubContentProvider update", () => {
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
); );
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -186,7 +188,7 @@ describe("GitHubContentProvider create", () => {
it("errors on invalid path", async () => { it("errors on invalid path", async () => {
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync"); spyOn(GitHubClient.prototype, "createOrUpdateFileAsync");
const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.create(undefined, "invalid path", sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled(); expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled();
@@ -195,7 +197,7 @@ describe("GitHubContentProvider create", () => {
it("errors on failed create", async () => { it("errors on failed create", async () => {
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
@@ -206,7 +208,7 @@ describe("GitHubContentProvider create", () => {
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit }) Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
); );
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(HttpStatusCodes.Created); expect(response.status).toBe(HttpStatusCodes.Created);
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
@@ -221,7 +223,7 @@ describe("GitHubContentProvider save", () => {
it("errors on invalid path", async () => { it("errors on invalid path", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync"); spyOn(GitHubClient.prototype, "getContentsAsync");
const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise(); const response = await gitHubContentProvider.save(undefined, "invalid path", undefined).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
expect(gitHubClient.getContentsAsync).not.toBeCalled(); expect(gitHubClient.getContentsAsync).not.toBeCalled();
@@ -230,7 +232,7 @@ describe("GitHubContentProvider save", () => {
it("errors on failed read", async () => { it("errors on failed read", async () => {
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.save(null, sampleGitHubUri, null).toPromise(); const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, undefined).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -242,7 +244,7 @@ describe("GitHubContentProvider save", () => {
); );
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 })); spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(888); expect(response.status).toBe(888);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -257,7 +259,7 @@ describe("GitHubContentProvider save", () => {
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
); );
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(gitHubClient.getContentsAsync).toBeCalled(); expect(gitHubClient.getContentsAsync).toBeCalled();
@@ -271,7 +273,7 @@ describe("GitHubContentProvider save", () => {
describe("GitHubContentProvider listCheckpoints", () => { describe("GitHubContentProvider listCheckpoints", () => {
it("errors for everything", async () => { it("errors for everything", async () => {
const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise(); const response = await gitHubContentProvider.listCheckpoints().toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
}); });
@@ -279,7 +281,7 @@ describe("GitHubContentProvider listCheckpoints", () => {
describe("GitHubContentProvider createCheckpoint", () => { describe("GitHubContentProvider createCheckpoint", () => {
it("errors for everything", async () => { it("errors for everything", async () => {
const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise(); const response = await gitHubContentProvider.createCheckpoint().toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
}); });
@@ -287,7 +289,7 @@ describe("GitHubContentProvider createCheckpoint", () => {
describe("GitHubContentProvider deleteCheckpoint", () => { describe("GitHubContentProvider deleteCheckpoint", () => {
it("errors for everything", async () => { it("errors for everything", async () => {
const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise(); const response = await gitHubContentProvider.deleteCheckpoint().toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
}); });
@@ -295,7 +297,7 @@ describe("GitHubContentProvider deleteCheckpoint", () => {
describe("GitHubContentProvider restoreFromCheckpoint", () => { describe("GitHubContentProvider restoreFromCheckpoint", () => {
it("errors for everything", async () => { it("errors for everything", async () => {
const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise(); const response = await gitHubContentProvider.restoreFromCheckpoint().toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
}); });

View File

@@ -1,15 +1,15 @@
import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable"; import { makeNotebookRecord, Notebook, stringifyNotebook, toJS } from "@nteract/commutable";
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core"; import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
import { from, Observable, of } from "rxjs"; import { from, Observable, of } from "rxjs";
import { AjaxResponse } from "rxjs/ajax"; import { AjaxResponse } from "rxjs/ajax";
import * as Base64Utils from "../Utils/Base64Utils";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
import * as GitHubUtils from "../Utils/GitHubUtils";
import * as UrlUtility from "../Common/UrlUtility";
import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import * as UrlUtility from "../Common/UrlUtility";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import * as Base64Utils from "../Utils/Base64Utils";
import * as GitHubUtils from "../Utils/GitHubUtils";
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
export interface GitHubContentProviderParams { export interface GitHubContentProviderParams {
gitHubClient: GitHubClient; gitHubClient: GitHubClient;
@@ -267,25 +267,25 @@ export class GitHubContentProvider implements IContentProvider {
); );
} }
public listCheckpoints(_: ServerConfig, path: string): Observable<AjaxResponse> { public listCheckpoints(): Observable<AjaxResponse> {
const error = new GitHubContentProviderError("Not implemented"); const error = new GitHubContentProviderError("Not implemented");
Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno); Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno);
return of(this.createErrorAjaxResponse(error)); return of(this.createErrorAjaxResponse(error));
} }
public createCheckpoint(_: ServerConfig, path: string): Observable<AjaxResponse> { public createCheckpoint(): Observable<AjaxResponse> {
const error = new GitHubContentProviderError("Not implemented"); const error = new GitHubContentProviderError("Not implemented");
Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno); Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno);
return of(this.createErrorAjaxResponse(error)); return of(this.createErrorAjaxResponse(error));
} }
public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> { public deleteCheckpoint(): Observable<AjaxResponse> {
const error = new GitHubContentProviderError("Not implemented"); const error = new GitHubContentProviderError("Not implemented");
Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno); Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno);
return of(this.createErrorAjaxResponse(error)); return of(this.createErrorAjaxResponse(error));
} }
public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> { public restoreFromCheckpoint(): Observable<AjaxResponse> {
const error = new GitHubContentProviderError("Not implemented"); const error = new GitHubContentProviderError("Not implemented");
Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno); Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno);
return of(this.createErrorAjaxResponse(error)); return of(this.createErrorAjaxResponse(error));

View File

@@ -1,22 +0,0 @@
import * as ko from "knockout";
import "../less/index.less";
import "./Libs/jquery";
class Index {
public navigationSelection: ko.Observable<string>;
constructor() {
this.navigationSelection = ko.observable("quickstart");
}
public quickstart_click() {
this.navigationSelection("quickstart");
}
public explorer_click() {
this.navigationSelection("explorer");
}
}
const index = new Index();
ko.applyBindings(index);

66
src/Index.tsx Normal file
View File

@@ -0,0 +1,66 @@
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Arrow from "../images/Arrow.svg";
import CosmosDB_20170829 from "../images/CosmosDB_20170829.svg";
import Explorer from "../images/Explorer.svg";
import Feedback from "../images/Feedback.svg";
import Quickstart from "../images/Quickstart.svg";
import "../less/index.less";
const Index = (): JSX.Element => {
const [navigationSelection, setNavigationSelection] = useState("quickstart");
const quickstart_click = () => {
setNavigationSelection("quickstart");
};
const explorer_click = () => {
setNavigationSelection("explorer");
};
return (
<React.Fragment>
<header className="header HeaderBg">
<div className="items">
<img className="DocDBicon" src={CosmosDB_20170829} alt="Azure Cosmos DB" />
<a className="createdocdbacnt" href="https://aka.ms/documentdbcreate" rel="noreferrer" target="_blank">
Create an Azure Cosmos DB account <img className="rightarrowimg" src={Arrow} alt="" />
</a>
<span className="title">Azure Cosmos DB Emulator</span>
</div>
</header>
<nav className="fixedleftpane">
<div
id="Quickstart"
onClick={quickstart_click}
className={navigationSelection === "quickstart" ? "topSelected" : ""}
>
<img id="imgiconwidth1" src={Quickstart} alt="Open Quick Start" />
<span className="menuQuickStart">Quickstart</span>
</div>
<div id="Explorer" onClick={explorer_click} className={navigationSelection === "explorer" ? "topSelected" : ""}>
<img id="imgiconwidth1" src={Explorer} alt="Open Data Explorer" />
<span className="menuExplorer">Explorer</span>
</div>
<div>
<a className="feedbackstyle" href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Emulator%20Feedback">
<img id="imgiconwidth1" src={Feedback} alt="Report Issue" />
<span className="menuExplorer">Report Issue</span>
</a>
</div>
</nav>
{navigationSelection === "quickstart" && (
<iframe name="quickstart" className="iframe" src="quickstart.html"></iframe>
)}
{navigationSelection === "explorer" && (
<iframe name="explorer" className="iframe" src="explorer.html?platform=Emulator"></iframe>
)}
</React.Fragment>
);
};
ReactDOM.render(<Index />, document.getElementById("root"));

View File

@@ -364,7 +364,6 @@ describe("Gallery", () => {
const name = "name"; const name = "name";
const description = "description"; const description = "description";
const tags = ["tag"]; const tags = ["tag"];
const author = "author";
const thumbnailUrl = "thumbnailUrl"; const thumbnailUrl = "thumbnailUrl";
const content = `{ "key": "value" }`; const content = `{ "key": "value" }`;
const addLinkToNotebookViewer = true; const addLinkToNotebookViewer = true;
@@ -373,7 +372,7 @@ describe("Gallery", () => {
json: () => undefined as any, json: () => undefined as any,
}); });
const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content); const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content);
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
@@ -391,7 +390,6 @@ describe("Gallery", () => {
name, name,
description, description,
tags, tags,
author,
thumbnailUrl, thumbnailUrl,
content: JSON.parse(content), content: JSON.parse(content),
addLinkToNotebookViewer, addLinkToNotebookViewer,

View File

@@ -61,7 +61,6 @@ export interface IPublishNotebookRequest {
name: string; name: string;
description: string; description: string;
tags: string[]; tags: string[];
author: string;
thumbnailUrl: string; thumbnailUrl: string;
content: any; content: any;
addLinkToNotebookViewer: boolean; addLinkToNotebookViewer: boolean;
@@ -359,7 +358,6 @@ export class JunoClient {
name: string, name: string,
description: string, description: string,
tags: string[], tags: string[],
author: string,
thumbnailUrl: string, thumbnailUrl: string,
content: string content: string
): Promise<IJunoResponse<IGalleryItem>> { ): Promise<IJunoResponse<IGalleryItem>> {
@@ -370,7 +368,6 @@ export class JunoClient {
name, name,
description, description,
tags, tags,
author,
thumbnailUrl, thumbnailUrl,
content: JSON.parse(content), content: JSON.parse(content),
addLinkToNotebookViewer: true, addLinkToNotebookViewer: true,

View File

@@ -40,7 +40,7 @@
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"Cost": "Cost", "ApproximateCost": "Approximate Cost Per Hour",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String", "ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",

View File

@@ -26,7 +26,7 @@ import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less"; import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less"; import "../less/tree.less";
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
import { ResourceTree } from "./Common/ResourceTree"; import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog } from "./Explorer/Controls/Dialog"; import { Dialog } from "./Explorer/Controls/Dialog";
@@ -84,7 +84,11 @@ const App: React.FunctionComponent = () => {
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree"> <div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter"> <div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */} {/* Collections Tree Expanded - Start */}
<ResourceTree toggleLeftPaneExpanded={toggleLeftPaneExpanded} isLeftPaneExpanded={isLeftPaneExpanded} /> <ResourceTreeContainer
container={explorer}
toggleLeftPaneExpanded={toggleLeftPaneExpanded}
isLeftPaneExpanded={isLeftPaneExpanded}
/>
{/* Collections Tree Expanded - End */} {/* Collections Tree Expanded - End */}
{/* Collections Tree Collapsed - Start */} {/* Collections Tree Collapsed - Start */}
<CollapsedResourceTree <CollapsedResourceTree

View File

@@ -26,7 +26,7 @@ export const SwitchAccount: FunctionComponent<Props> = ({
data: account, data: account,
}))} }))}
onChange={(_, option) => { onChange={(_, option) => {
setSelectedAccountName(String(option.key)); setSelectedAccountName(String(option?.key));
dismissMenu(); dismissMenu();
}} }}
defaultSelectedKey={selectedAccount?.name} defaultSelectedKey={selectedAccount?.name}

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