Resolved conflicts and Master merged

This commit is contained in:
vaidankarswapnil 2021-08-11 13:13:46 +05:30
commit 046e6eb5a4
92 changed files with 6309 additions and 4885 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

@ -191,4 +191,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

@ -2,6 +2,7 @@
.dataResourceTree { .dataResourceTree {
margin-left: @MediumSpace; margin-left: @MediumSpace;
overflow: auto;
.databaseHeader { .databaseHeader {
font-size: 14px; font-size: 14px;

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,14 +44,13 @@
} }
.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;
@ -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 {

29
package-lock.json generated
View File

@ -5583,6 +5583,11 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
}, },
"@types/lodash": {
"version": "4.14.171",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz",
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg=="
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -20618,9 +20623,9 @@
} }
}, },
"playwright": { "playwright": {
"version": "1.10.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.10.0.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.13.0.tgz",
"integrity": "sha512-b7SGBcCPq4W3pb4ImEDmNXtO0ZkJbZMuWiShsaNJd+rGfY/6fqwgllsAojmxGSgFmijYw7WxCoPiAIEDIH16Kw==", "integrity": "sha512-GA5OyEeKx1v/pRcANmYncCT67Y7Y4N5zLRU5E690dn/Id10sooR5hQZmCDYsjXlutZb/1q0R3sITALnvhEjCjg==",
"dev": true, "dev": true,
"requires": { "requires": {
"commander": "^6.1.0", "commander": "^6.1.0",
@ -20635,7 +20640,8 @@
"proxy-from-env": "^1.1.0", "proxy-from-env": "^1.1.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stack-utils": "^2.0.3", "stack-utils": "^2.0.3",
"ws": "^7.3.1" "ws": "^7.4.6",
"yazl": "^2.5.1"
}, },
"dependencies": { "dependencies": {
"commander": { "commander": {
@ -20667,6 +20673,12 @@
"requires": { "requires": {
"escape-string-regexp": "^2.0.0" "escape-string-regexp": "^2.0.0"
} }
},
"ws": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
"integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"dev": true
} }
} }
}, },
@ -26157,6 +26169,15 @@
"fd-slicer": "~1.1.0" "fd-slicer": "~1.1.0"
} }
}, },
"yazl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
"dev": true,
"requires": {
"buffer-crc32": "~0.2.3"
}
},
"yocto-queue": { "yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -42,6 +42,7 @@
"@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",
@ -163,7 +164,7 @@
"mini-css-extract-plugin": "0.4.3", "mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0", "monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"playwright": "1.10.0", "playwright": "1.13.0",
"prettier": "2.2.1", "prettier": "2.2.1",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react-dev-utils": "11.0.4", "react-dev-utils": "11.0.4",

View File

@ -95,6 +95,7 @@ export class Flights {
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
} }
export class AfecFeatures { export class AfecFeatures {

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

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

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

@ -42,19 +42,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
}, },
@ -122,19 +109,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [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";
@ -35,7 +34,7 @@ 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 "./ComponentRegisterer"; import "./ComponentRegisterer";
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog"; 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) {
@ -362,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;
@ -540,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;
} }
@ -616,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.
@ -720,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");
@ -734,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",
@ -751,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}
/> />
@ -759,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");
@ -781,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}
/> />
@ -842,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");
@ -862,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();
} }
@ -879,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)}`)
); );
@ -888,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");
@ -903,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(
@ -932,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) {

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;
@ -110,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();
} }
@ -141,6 +144,7 @@ export default class NotebookManager {
notebookContentRef={notebookContentRef} notebookContentRef={notebookContentRef}
onTakeSnapshot={onTakeSnapshot} onTakeSnapshot={onTakeSnapshot}
/>, />,
"440px",
onClosePanel onClosePanel
); );
} }
@ -169,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",
@ -193,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

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

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

@ -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,11 +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(),
(userContext.features.partitionKeyDefault && userContext.apiType === "SQL") ||
(userContext.features.partitionKeyDefault && userContext.apiType === "Mongo")
? "/id"
: "",
enableDedicatedThroughput: false, enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"), createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
useHashV2: false, useHashV2: false,
@ -815,6 +811,19 @@ 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 { private getPartitionKeySubtext(): string {
if ( if (
userContext.features.partitionKeyDefault && userContext.features.partitionKeyDefault &&

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

@ -31,19 +31,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [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

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

@ -21,19 +21,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"copyNotebook": [Function], "copyNotebook": [Function],
"gitHubOAuthService": GitHubOAuthService {
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"token": [Function],
},
"junoClient": JunoClient {
"cachedPinnedRepos": [Function],
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function], "parameters": [Function],
}, },
} }

View File

@ -1,4 +1,4 @@
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 AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
@ -13,8 +13,7 @@ import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../../Tables/TableDataClient";
import * as Utilities from "../../Tables/Utilities"; import * as Utilities from "../../Tables/Utilities";
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab"; import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
// import QueryTablesTab from "../../Tabs/QueryTablesTab"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { PanelContainerComponent } from "../PanelContainerComponent";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@ -29,9 +28,7 @@ import {
getCassandraDefaultEntities, getCassandraDefaultEntities,
getDefaultEntities, getDefaultEntities,
getEntityValuePlaceholder, getEntityValuePlaceholder,
getPanelTitle,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
@ -72,6 +69,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(() => {
@ -100,12 +99,21 @@ 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);
await tableDataClient.createDocument(queryTablesTab.collection, entity); await tableDataClient.createDocument(queryTablesTab.collection, entity);
// await tableEntityListViewModel.addEntityToCache(newEntity); // await tableEntityListViewModel.addEntityToCache(newEntity);
@ -172,10 +180,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 (
@ -221,61 +254,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,4 +1,4 @@
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";
@ -8,14 +8,12 @@ import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel"; 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 TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel"; import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities"; 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 NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab"; import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
// import QueryTablesTab from "../../Tabs/QueryTablesTab"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { PanelContainerComponent } from "../PanelContainerComponent";
import { import {
attributeNameLabel, attributeNameLabel,
attributeValueLabel, attributeValueLabel,
@ -30,14 +28,12 @@ import {
getEntityValuePlaceholder, getEntityValuePlaceholder,
getFormattedTime, getFormattedTime,
imageProps, imageProps,
isValidEntities,
options, options,
} from "./Validators/EntityTableHelper"; } from "./Validators/EntityTableHelper";
interface EditTableEntityPanelProps { interface EditTableEntityPanelProps {
tableDataClient: TableDataClient; tableDataClient: TableDataClient;
queryTablesTab: NewQueryTablesTab; queryTablesTab: NewQueryTablesTab;
// queryTablesTab: QueryTablesTab;
tableEntityListViewModel: TableEntityListViewModel; tableEntityListViewModel: TableEntityListViewModel;
cassandraApiClient: CassandraAPIDataClient; cassandraApiClient: CassandraAPIDataClient;
selectedEntity: Entities.ITableEntity[]; selectedEntity: Entities.ITableEntity[];
@ -71,6 +67,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
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,
@ -92,6 +90,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
originalDocument = entityAttribute; originalDocument = entityAttribute;
} }
setOriginalDocument(originalDocument); setOriginalDocument(originalDocument);
//eslint-disable-next-line
}, []); }, []);
const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => { const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => {
@ -196,53 +195,29 @@ 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;
await newTableDataClient.updateDocument(queryTablesTab.collection, originalDocumentData, entity); await newTableDataClient.updateDocument(queryTablesTab.collection, originalDocumentData, entity);
// await tableEntityListViewModel.updateCachedEntity(newEntity);
// if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
// tableEntityListViewModel.redrawTableThrottled();
reloadEntities(); reloadEntities();
// }
// tableEntityListViewModel.selected.removeAll();
// tableEntityListViewModel.selected.push(newEntity);
closeSidePanel(); closeSidePanel();
}; };
// const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
// let newHeaders: string[] = [];
// const keys = Object.keys(newEntity);
// keys &&
// keys.forEach((key: string) => {
// if (
// !_.contains(viewModel.headers, key) &&
// key !== TableEntityProcessor.keyProperties.attachments &&
// key !== TableEntityProcessor.keyProperties.etag &&
// key !== TableEntityProcessor.keyProperties.resourceId &&
// key !== TableEntityProcessor.keyProperties.self &&
// (!(userContext.apiType === "Cassandra") || key !== TableConstants.EntityKeyNames.RowKey)
// ) {
// newHeaders.push(key);
// }
// });
// let newHeadersInserted = false;
// if (newHeaders.length) {
// if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
// newHeaders = viewModel.headers.concat(newHeaders);
// }
// viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
// newHeadersInserted = true;
// }
// return newHeadersInserted;
// };
// Add new entity row // Add new entity row
const addNewEntity = (): void => { const addNewEntity = (): void => {
const cloneEntities = [...entities]; const cloneEntities = [...entities];
@ -302,10 +277,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 (
@ -352,59 +352,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

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

@ -0,0 +1,285 @@
import * as ko from "knockout";
import React from "react";
import AddEntityIcon from "../../../images/AddEntity.svg";
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
import EditEntityIcon from "../../../images/Edit-entity.svg";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
import QueryTextIcon from "../../../images/Query-Text.svg";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "../Panes/Tables/EditTableEntityPanel";
import TableCommands from "../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient } from "../Tables/TableDataClient";
import template from "./QueryTablesTab.html";
import TabsBase from "./TabsBase";
// Will act as table explorer class
export default class QueryTablesTab extends TabsBase {
public readonly html = template;
public collection: ViewModels.Collection;
public tableEntityListViewModel = ko.observable<TableEntityListViewModel>();
public queryViewModel = ko.observable<QueryViewModel>();
public tableCommands: TableCommands;
public tableDataClient: TableDataClient;
public queryText = ko.observable("PartitionKey eq 'partitionKey1'"); // Start out with an example they can modify
public selectedQueryText = ko.observable("").extend({ notify: "always" });
public executeQueryButton: ViewModels.Button;
public addEntityButton: ViewModels.Button;
public editEntityButton: ViewModels.Button;
public deleteEntityButton: ViewModels.Button;
public queryBuilderButton: ViewModels.Button;
public queryTextButton: ViewModels.Button;
public container: Explorer;
constructor(options: ViewModels.TabOptions) {
super(options);
this.container = options.collection && options.collection.container;
this.tableCommands = new TableCommands(this.container);
this.tableDataClient = this.container.tableDataClient;
this.tableEntityListViewModel(new TableEntityListViewModel(this.tableCommands, this));
this.tableEntityListViewModel().queryTablesTab = this;
this.queryViewModel(new QueryViewModel(this));
const sampleQuerySubscription = this.tableEntityListViewModel().items.subscribe(() => {
if (this.tableEntityListViewModel().items().length > 0 && userContext.apiType === "Tables") {
this.queryViewModel().queryBuilderViewModel().setExample();
}
sampleQuerySubscription.dispose();
});
this.executeQueryButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.queryBuilderButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
isSelected: ko.computed<boolean>(() => {
return this.queryViewModel() ? this.queryViewModel().isHelperActive() : false;
}),
};
this.queryTextButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
isSelected: ko.computed<boolean>(() => {
return this.queryViewModel() ? this.queryViewModel().isEditorActive() : false;
}),
};
this.addEntityButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.editEntityButton = {
enabled: ko.computed<boolean>(() => {
return this.tableCommands.isEnabled(
TableCommands.editEntityCommand,
this.tableEntityListViewModel().selected()
);
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.deleteEntityButton = {
enabled: ko.computed<boolean>(() => {
return this.tableCommands.isEnabled(
TableCommands.deleteEntitiesCommand,
this.tableEntityListViewModel().selected()
);
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.buildCommandBarOptions();
}
public onAddEntityClick = (): void => {
useSidePanel
.getState()
.openSidePanel(
"Add Table Row",
<AddTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>,
"700px"
);
};
public onEditEntityClick = (): void => {
useSidePanel
.getState()
.openSidePanel(
"Edit Table Entity",
<EditTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>,
"700px"
);
};
public onDeleteEntityClick = (): void => {
this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel());
};
public onActivate(): void {
super.onActivate();
const columns =
!!this.tableEntityListViewModel() &&
!!this.tableEntityListViewModel().table &&
this.tableEntityListViewModel().table.columns;
if (columns) {
columns.adjust();
$(window).resize();
}
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (this.queryBuilderButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "CQL Query Builder" : "Query Builder";
buttons.push({
iconSrc: QueryBuilderIcon,
iconAlt: label,
onCommandClick: () => this.queryViewModel().selectHelper(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.queryBuilderButton.enabled(),
isSelected: this.queryBuilderButton.isSelected(),
});
}
if (this.queryTextButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "CQL Query Text" : "Query Text";
buttons.push({
iconSrc: QueryTextIcon,
iconAlt: label,
onCommandClick: () => this.queryViewModel().selectEditor(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.queryTextButton.enabled(),
isSelected: this.queryTextButton.isSelected(),
});
}
if (this.executeQueryButton.visible()) {
const label = "Run Query";
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: () => this.queryViewModel().runQuery(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.executeQueryButton.enabled(),
});
}
if (this.addEntityButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "Add Row" : "Add Entity";
buttons.push({
iconSrc: AddEntityIcon,
iconAlt: label,
onCommandClick: this.onAddEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.addEntityButton.enabled(),
});
}
if (this.editEntityButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "Edit Row" : "Edit Entity";
buttons.push({
iconSrc: EditEntityIcon,
iconAlt: label,
onCommandClick: this.onEditEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.editEntityButton.enabled(),
});
}
if (this.deleteEntityButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "Delete Rows" : "Delete Entities";
buttons.push({
iconSrc: DeleteEntitiesIcon,
iconAlt: label,
onCommandClick: this.onDeleteEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.deleteEntityButton.enabled(),
});
}
return buttons;
}
protected buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([
this.queryBuilderButton.visible,
this.queryBuilderButton.enabled,
this.queryTextButton.visible,
this.queryTextButton.enabled,
this.executeQueryButton.visible,
this.executeQueryButton.enabled,
this.addEntityButton.visible,
this.addEntityButton.enabled,
this.editEntityButton.visible,
this.editEntityButton.enabled,
this.deleteEntityButton.visible,
this.deleteEntityButton.enabled,
])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
}

View File

@ -615,8 +615,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",

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,748 @@
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 } 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 (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 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()) {
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

@ -16,10 +16,9 @@ import { Areas } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
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 { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@ -28,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";
@ -56,8 +56,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
public galleryContentRoot: NotebookContentItem; public galleryContentRoot: NotebookContentItem;
public myNotebooksContentRoot: NotebookContentItem; public myNotebooksContentRoot: NotebookContentItem;
public gitHubNotebooksContentRoot: NotebookContentItem; public gitHubNotebooksContentRoot: NotebookContentItem;
public junoClient: JunoClient;
public gitHubOAuthService: GitHubOAuthService;
public constructor(private container: Explorer) { public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now()); this.parameters = ko.observable(Date.now());
@ -74,8 +72,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
useDatabases.subscribe(() => this.triggerRender()); useDatabases.subscribe(() => this.triggerRender());
this.triggerRender(); this.triggerRender();
this.junoClient = new JunoClient();
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
} }
private traceMyNotebookTreeInfo() { private traceMyNotebookTreeInfo() {
@ -639,7 +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.junoClient} junoClientProp={this.container.notebookManager.junoClient}
/> />
), ),
}, },
@ -717,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",
@ -782,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";
@ -95,10 +96,11 @@ 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);
@ -108,5 +110,9 @@ export default class UserDefinedFunction {
/**/ /**/
} }
); );
},
"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

@ -1,23 +0,0 @@
import "../less/index.less";
import "./Libs/jquery";
import * as ko from "knockout";
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");
}
}
var index = new Index();
ko.applyBindings(index);

View File

@ -23,7 +23,7 @@ const Index = (): JSX.Element => {
<header className="header HeaderBg"> <header className="header HeaderBg">
<div className="items"> <div className="items">
<img className="DocDBicon" src={CosmosDB_20170829} alt="Azure Cosmos DB" /> <img className="DocDBicon" src={CosmosDB_20170829} alt="Azure Cosmos DB" />
<a className="createdocdbacnt" href="https://aka.ms/documentdbcreate" target="_blank" rel="noreferrer"> <a className="createdocdbacnt" href="https://aka.ms/documentdbcreate" rel="noreferrer" target="_blank">
Create an Azure Cosmos DB account <img className="rightarrowimg" src={Arrow} alt="" /> Create an Azure Cosmos DB account <img className="rightarrowimg" src={Arrow} alt="" />
</a> </a>
<span className="title">Azure Cosmos DB Emulator</span> <span className="title">Azure Cosmos DB Emulator</span>

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

@ -10,11 +10,13 @@ export type Features = {
readonly enableSchema: boolean; readonly enableSchema: boolean;
autoscaleDefault: boolean; autoscaleDefault: boolean;
partitionKeyDefault: boolean; partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
readonly executeSproc: boolean; readonly executeSproc: boolean;
readonly enableAadDataPlane: boolean; readonly enableAadDataPlane: boolean;
readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly livyEndpoint?: string; readonly livyEndpoint?: string;
@ -56,6 +58,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableSDKoperations: "true" === get("enablesdkoperations"), enableSDKoperations: "true" === get("enablesdkoperations"),
enableSpark: "true" === get("enablespark"), enableSpark: "true" === get("enablespark"),
enableTtl: "true" === get("enablettl"), enableTtl: "true" === get("enablettl"),
enableKoResourceTree: "true" === get("enablekoresourcetree"),
executeSproc: "true" === get("dataexplorerexecutesproc"), executeSproc: "true" === get("dataexplorerexecutesproc"),
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
junoEndpoint: get("junoendpoint"), junoEndpoint: get("junoendpoint"),
@ -70,5 +73,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
ttl90Days: "true" === get("ttl90days"), ttl90Days: "true" === get("ttl90days"),
autoscaleDefault: "true" === get("autoscaledefault"), autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
}; };
} }

View File

@ -4,7 +4,12 @@ import { armRequestWithoutPolling } from "../../Utils/arm/request";
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import { RefreshResult } from "../SelfServeTypes"; import { RefreshResult } from "../SelfServeTypes";
import SqlX from "./SqlX"; import SqlX from "./SqlX";
import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; import {
FetchPricesResponse,
RegionsResponse,
SqlxServiceResource,
UpdateDedicatedGatewayRequestParameters,
} from "./SqlxTypes";
const apiVersion = "2021-04-01-preview"; const apiVersion = "2021-04-01-preview";
@ -128,3 +133,67 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResu
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
} }
}; };
const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
};
export const getReadRegions = async (): Promise<Array<string>> => {
try {
const readRegions = new Array<string>();
const response = await armRequestWithoutPolling<RegionsResponse>({
host: configContext.ARM_ENDPOINT,
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
method: "GET",
apiVersion: "2021-04-01-preview",
});
if (response.result.location !== undefined) {
readRegions.push(response.result.location.replace(" ", "").toLowerCase());
} else {
for (const location of response.result.locations) {
readRegions.push(location.locationName.replace(" ", "").toLowerCase());
}
}
return readRegions;
} catch (err) {
return new Array<string>();
}
};
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
};
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
try {
const priceMap = new Map<string, Map<string, number>>();
for (const region of regions) {
const regionPriceMap = new Map<string, number>();
const response = await armRequestWithoutPolling<FetchPricesResponse>({
host: configContext.ARM_ENDPOINT,
path: getFetchPricesPathForRegion(userContext.subscriptionId),
method: "POST",
apiVersion: "2020-01-01-preview",
queryParams: {
filter:
"armRegionNameeq '" +
region +
"'andserviceFamilyeq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
},
});
for (const item of response.result.Items) {
regionPriceMap.set(item.skuName, item.retailPrice);
}
priceMap.set(region, regionPriceMap);
}
return priceMap;
} catch (err) {
return undefined;
}
};

View File

@ -16,11 +16,13 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
import { import {
deleteDedicatedGatewayResource, deleteDedicatedGatewayResource,
getCurrentProvisioningState, getCurrentProvisioningState,
getPriceMap,
getReadRegions,
refreshDedicatedGatewayProvisioning, refreshDedicatedGatewayProvisioning,
updateDedicatedGatewayResource, updateDedicatedGatewayResource,
} from "./SqlX.rp"; } from "./SqlX.rp";
const costPerHourValue: Description = { const costPerHourDefaultValue: Description = {
textTKey: "CostText", textTKey: "CostText",
type: DescriptionType.Text, type: DescriptionType.Text,
link: { link: {
@ -53,7 +55,10 @@ const CosmosD16s = "Cosmos.D16s";
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => { const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentValues.set("sku", { value: newValue }); currentValues.set("sku", { value: newValue });
currentValues.set("costPerHour", { value: costPerHourValue }); currentValues.set("costPerHour", {
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
});
return currentValues; return currentValues;
}; };
@ -79,6 +84,11 @@ const onNumberOfInstancesChange = (
} else { } else {
currentValues.set("warningBanner", undefined); currentValues.set("warningBanner", undefined);
} }
currentValues.set("costPerHour", {
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
});
return currentValues; return currentValues;
}; };
@ -111,6 +121,11 @@ const onEnableDedicatedGatewayChange = (
} as Description, } as Description,
hidden: false, hidden: false,
}); });
currentValues.set("costPerHour", {
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
hidden: false,
});
} else { } else {
currentValues.set("warningBanner", { currentValues.set("warningBanner", {
value: { value: {
@ -122,6 +137,8 @@ const onEnableDedicatedGatewayChange = (
} as Description, } as Description,
hidden: false, hidden: false,
}); });
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
} }
const sku = currentValues.get("sku"); const sku = currentValues.get("sku");
const instances = currentValues.get("instances"); const instances = currentValues.get("instances");
@ -137,7 +154,6 @@ const onEnableDedicatedGatewayChange = (
disabled: dedicatedGatewayOriginallyEnabled, disabled: dedicatedGatewayOriginallyEnabled,
}); });
currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes });
currentValues.set("connectionString", { currentValues.set("connectionString", {
value: connectionStringValue, value: connectionStringValue,
hidden: !newValue || !dedicatedGatewayOriginallyEnabled, hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
@ -177,6 +193,40 @@ const NumberOfInstancesDropdownInfo: Info = {
}, },
}; };
const ApproximateCostDropDownInfo: Info = {
messageTKey: "CostText",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
textTKey: "DedicatedGatewayPricing",
},
};
let priceMap: Map<string, Map<string, number>>;
let regions: Array<string>;
const calculateCost = (skuName: string, instanceCount: number): Description => {
try {
let costPerHour = 0;
for (const region of regions) {
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
if (incrementalCost === undefined) {
throw new Error("Value not found in map.");
}
costPerHour += incrementalCost;
}
costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100;
return {
textTKey: `${costPerHour} USD`,
type: DescriptionType.Text,
};
} catch (err) {
return costPerHourDefaultValue;
}
};
@IsDisplayable() @IsDisplayable()
@RefreshOptions({ retryIntervalInMs: 20000 }) @RefreshOptions({ retryIntervalInMs: 20000 })
export default class SqlX extends SelfServeBaseClass { export default class SqlX extends SelfServeBaseClass {
@ -274,12 +324,15 @@ export default class SqlX extends SelfServeBaseClass {
hidden: true, hidden: true,
}); });
regions = await getReadRegions();
priceMap = await getPriceMap(regions);
const response = await getCurrentProvisioningState(); const response = await getCurrentProvisioningState();
if (response.status && response.status !== "Deleting") { if (response.status && response.status !== "Deleting") {
defaults.set("enableDedicatedGateway", { value: true }); defaults.set("enableDedicatedGateway", { value: true });
defaults.set("sku", { value: response.sku, disabled: true }); defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("instances", { value: response.instances, disabled: false }); defaults.set("instances", { value: response.instances, disabled: false });
defaults.set("costPerHour", { value: costPerHourValue }); defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
defaults.set("connectionString", { defaults.set("connectionString", {
value: connectionStringValue, value: connectionStringValue,
hidden: false, hidden: false,
@ -338,8 +391,9 @@ export default class SqlX extends SelfServeBaseClass {
}) })
instances: number; instances: number;
@PropertyInfo(ApproximateCostDropDownInfo)
@Values({ @Values({
labelTKey: "Cost", labelTKey: "ApproximateCost",
isDynamicDescription: true, isDynamicDescription: true,
}) })
costPerHour: string; costPerHour: string;

View File

@ -29,3 +29,23 @@ export type UpdateDedicatedGatewayRequestProperties = {
instanceCount: number; instanceCount: number;
serviceType: string; serviceType: string;
}; };
export type FetchPricesResponse = {
Items: Array<PriceItem>;
NextPageLink: string | undefined;
Count: number;
};
export type PriceItem = {
retailPrice: number;
skuName: string;
};
export type RegionsResponse = {
locations: Array<RegionItem>;
location: string;
};
export type RegionItem = {
locationName: string;
};

View File

@ -1,8 +1,9 @@
import * as GalleryUtils from "./GalleryUtils";
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { useDialog } from "../Explorer/Controls/Dialog";
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "./GalleryUtils";
const galleryItem: IGalleryItem = { const galleryItem: IGalleryItem = {
id: "id", id: "id",
@ -29,11 +30,11 @@ describe("GalleryUtils", () => {
it("downloadItem shows dialog in data explorer", () => { it("downloadItem shows dialog in data explorer", () => {
const container = {} as Explorer; const container = {} as Explorer;
container.showOkCancelModalDialog = jest.fn().mockImplementation();
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
expect(container.showOkCancelModalDialog).toBeCalled(); expect(useDialog.getState().visible).toBe(true);
expect(useDialog.getState().dialogProps).toBeDefined();
expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks");
}); });
it("favoriteItem favorites item", async () => { it("favoriteItem favorites item", async () => {
@ -66,11 +67,11 @@ describe("GalleryUtils", () => {
it("deleteItem shows dialog in data explorer", () => { it("deleteItem shows dialog in data explorer", () => {
const container = {} as Explorer; const container = {} as Explorer;
container.showOkCancelModalDialog = jest.fn().mockImplementation();
GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); GalleryUtils.deleteItem(container, undefined, galleryItem, undefined);
expect(container.showOkCancelModalDialog).toBeCalled(); expect(useDialog.getState().visible).toBe(true);
expect(useDialog.getState().dialogProps).toBeDefined();
expect(useDialog.getState().dialogProps.title).toBe("Remove published notebook");
}); });
it("getGalleryViewerProps gets gallery viewer props correctly", () => { it("getGalleryViewerProps gets gallery viewer props correctly", () => {

View File

@ -3,7 +3,7 @@ import { Notebook } from "@nteract/commutable";
import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import { TextFieldProps } from "../Explorer/Controls/Dialog"; import { TextFieldProps, useDialog } from "../Explorer/Controls/Dialog";
import { import {
GalleryTab, GalleryTab,
GalleryViewerComponent, GalleryViewerComponent,
@ -222,7 +222,7 @@ export function downloadItem(
}); });
const name = data.name; const name = data.name;
container.showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
"Download to My Notebooks", "Download to My Notebooks",
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
"Download", "Download",
@ -243,6 +243,11 @@ export function downloadItem(
const notebook = JSON.parse(response.data) as Notebook; const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId); removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook)); await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`); logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`);
@ -388,7 +393,7 @@ export function deleteItem(
if (container) { if (container) {
trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id }); trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id });
container.showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
"Remove published notebook", "Remove published notebook",
`Would you like to remove ${data.name} from the gallery?`, `Would you like to remove ${data.name} from the gallery?`,
"Remove", "Remove",

View File

@ -29,7 +29,13 @@ export async function update(
body: Types.DatabaseAccountUpdateParameters body: Types.DatabaseAccountUpdateParameters
): Promise<Types.DatabaseAccountGetResults> { ): Promise<Types.DatabaseAccountGetResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body }); return armRequest({
host: configContext.ARM_ENDPOINT,
path,
method: "PATCH",
apiVersion,
body,
});
} }
/* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */ /* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */

View File

@ -6,6 +6,7 @@ Instead, generate ARM clients that consume this function with stricter typing.
*/ */
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { HttpHeaders } from "../../Common/Constants";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@ -45,6 +46,7 @@ interface Options {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"; method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
body?: unknown; body?: unknown;
queryParams?: ARMQueryParams; queryParams?: ARMQueryParams;
contentType?: string;
} }
export async function armRequestWithoutPolling<T>({ export async function armRequestWithoutPolling<T>({
@ -54,6 +56,7 @@ export async function armRequestWithoutPolling<T>({
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
contentType,
}: Options): Promise<{ result: T; operationStatusUrl: string }> { }: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host); const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
@ -70,6 +73,7 @@ export async function armRequestWithoutPolling<T>({
method, method,
headers: { headers: {
Authorization: userContext.authorizationToken, Authorization: userContext.authorizationToken,
[HttpHeaders.contentType]: contentType || "application/json",
}, },
body: requestBody ? JSON.stringify(requestBody) : undefined, body: requestBody ? JSON.stringify(requestBody) : undefined,
}); });
@ -104,6 +108,7 @@ export async function armRequest<T>({
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
contentType,
}: Options): Promise<T> { }: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({ const armRequestResult = await armRequestWithoutPolling<T>({
host, host,
@ -112,6 +117,7 @@ export async function armRequest<T>({
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
contentType,
}); });
const operationStatusUrl = armRequestResult.operationStatusUrl; const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) { if (operationStatusUrl) {

View File

@ -331,6 +331,12 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.PartitionKeyTest) !== -1) { if (inputs.flights.indexOf(Flights.PartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault = true; userContext.features.partitionKeyDefault = true;
} }
if (inputs.flights.indexOf(Flights.PartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault = true;
}
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault2 = true;
}
} }
} }

View File

@ -2,14 +2,17 @@ import create, { UseStore } from "zustand";
export interface SidePanelState { export interface SidePanelState {
isOpen: boolean; isOpen: boolean;
panelWidth: string;
panelContent?: JSX.Element; panelContent?: JSX.Element;
headerText?: string; headerText?: string;
openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void; openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
closeSidePanel: () => void; closeSidePanel: () => void;
} }
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({ export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
isOpen: false, isOpen: false,
openSidePanel: (headerText, panelContent) => set((state) => ({ ...state, headerText, panelContent, isOpen: true })), panelWidth: "440px",
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
closeSidePanel: () => set((state) => ({ ...state, isOpen: false })), closeSidePanel: () => set((state) => ({ ...state, isOpen: false })),
})); }));

View File

@ -1,18 +1,17 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Cassandra keyspace and table CRUD", async () => { test("Cassandra keyspace and table CRUD", async () => {
const keyspaceId = generateUniqueName("keyspace"); const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table"); const tableId = generateUniqueName("table");
page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner");
await page.waitForSelector("iframe"); await page.waitForSelector("iframe");
const explorer = page.frame({ const explorer = await waitForExplorer();
name: "explorer",
});
await explorer.click('[data-test="New Table"]'); await explorer.click('[data-test="New Table"]');
await explorer.click('[aria-label="Keyspace id"]'); await explorer.click('[aria-label="Keyspace id"]');
@ -20,9 +19,9 @@ test("Cassandra keyspace and table CRUD", async () => {
await explorer.click('[aria-label="addCollection-tableId"]'); await explorer.click('[aria-label="addCollection-tableId"]');
await explorer.fill('[aria-label="addCollection-tableId"]', tableId); await explorer.fill('[aria-label="addCollection-tableId"]', tableId);
await explorer.click("#sidePanelOkButton"); await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`); await explorer.click(`.nodeItem >> text=${keyspaceId}`);
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); await explorer.click('button[role="menuitem"]:has-text("Delete Table")');
await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId);
await explorer.click('[aria-label="OK"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`); await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`);

View File

@ -1,18 +1,16 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Graph CRUD", async () => { test("Graph CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner");
await page.waitForSelector("iframe"); const explorer = await waitForExplorer();
const explorer = page.frame({
name: "explorer",
});
// Create new database and graph // Create new database and graph
await explorer.click('[data-test="New Graph"]'); await explorer.click('[data-test="New Graph"]');
@ -20,11 +18,11 @@ test("Graph CRUD", async () => {
await explorer.fill('[aria-label="Graph id"]', containerId); await explorer.fill('[aria-label="Graph id"]', containerId);
await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.fill('[aria-label="Partition key"]', "/pk");
await explorer.click("#sidePanelOkButton"); await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `.nodeItem >> text=${containerId}`); await explorer.click(`.nodeItem >> text=${containerId}`);
// Delete database and graph // Delete database and graph
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Graph")'); await explorer.click('button[role="menuitem"]:has-text("Delete Graph")');
await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId);
await explorer.click('[aria-label="OK"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);

View File

@ -1,18 +1,16 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
await page.waitForSelector("iframe"); const explorer = await waitForExplorer();
const explorer = page.frame({
name: "explorer",
});
// Create new database and collection // Create new database and collection
await explorer.click('[data-test="New Collection"]'); await explorer.click('[data-test="New Collection"]');
@ -20,10 +18,10 @@ test("Mongo CRUD", async () => {
await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Collection id"]', containerId);
await explorer.fill('[aria-label="Shard key"]', "/pk"); await explorer.fill('[aria-label="Shard key"]', "/pk");
await explorer.click("#sidePanelOkButton"); await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `.nodeItem >> text=${containerId}`); await explorer.click(`.nodeItem >> text=${containerId}`);
// Create indexing policy // Create indexing policy
await safeClick(explorer, ".nodeItem >> text=Settings"); await explorer.click(".nodeItem >> text=Settings");
await explorer.click('button[role="tab"]:has-text("Indexing Policy")'); await explorer.click('button[role="tab"]:has-text("Indexing Policy")');
await explorer.click('[aria-label="Index Field Name 0"]'); await explorer.click('[aria-label="Index Field Name 0"]');
await explorer.fill('[aria-label="Index Field Name 0"]', "foo"); await explorer.fill('[aria-label="Index Field Name 0"]', "foo");
@ -34,8 +32,8 @@ test("Mongo CRUD", async () => {
await explorer.click('[aria-label="Delete index Button"]'); await explorer.click('[aria-label="Delete index Button"]');
await explorer.click('[data-test="Save"]'); await explorer.click('[data-test="Save"]');
// Delete database and collection // Delete database and collection
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); await explorer.click('button[role="menuitem"]:has-text("Delete Collection")');
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
await explorer.click('[aria-label="OK"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);

View File

@ -1,18 +1,16 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
await page.waitForSelector("iframe"); const explorer = await waitForExplorer();
const explorer = page.frame({
name: "explorer",
});
// Create new database and collection // Create new database and collection
await explorer.click('[data-test="New Collection"]'); await explorer.click('[data-test="New Collection"]');
@ -20,11 +18,11 @@ test("Mongo CRUD", async () => {
await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Collection id"]', containerId);
await explorer.fill('[aria-label="Shard key"]', "pk"); await explorer.fill('[aria-label="Shard key"]', "pk");
await explorer.click("#sidePanelOkButton"); await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `.nodeItem >> text=${databaseId}`); explorer.click(`.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `.nodeItem >> text=${containerId}`); explorer.click(`.nodeItem >> text=${containerId}`);
// Delete database and collection // Delete database and collection
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); explorer.click('button[role="menuitem"]:has-text("Delete Collection")');
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
await explorer.click('[aria-label="OK"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);

View File

@ -2,6 +2,7 @@ import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
const filename = "GettingStarted.ipynb"; const filename = "GettingStarted.ipynb";
@ -11,10 +12,7 @@ fs.copyFileSync(path.join(__dirname, filename), path.join(__dirname, fileToUploa
test("Notebooks", async () => { test("Notebooks", async () => {
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
await page.waitForSelector("iframe"); const explorer = await waitForExplorer();
const explorer = page.frame({
name: "explorer",
});
// Upload and Delete Notebook // Upload and Delete Notebook
await explorer.click('[data-test="My Notebooks"] [aria-label="More"]'); await explorer.click('[data-test="My Notebooks"] [aria-label="More"]');
await explorer.click('button[role="menuitem"]:has-text("Upload File")'); await explorer.click('button[role="menuitem"]:has-text("Upload File")');

View File

@ -1,27 +1,24 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("SQL CRUD", async () => { test("SQL CRUD", async () => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container"); const containerId = generateUniqueName("container");
page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
await page.waitForSelector("iframe"); const explorer = await waitForExplorer();
const explorer = page.frame({
name: "explorer",
});
await explorer.click('[data-test="New Container"]'); await explorer.click('[data-test="New Container"]');
await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="New database id"]', databaseId);
await explorer.fill('[aria-label="Container id"]', containerId); await explorer.fill('[aria-label="Container id"]', containerId);
await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.fill('[aria-label="Partition key"]', "/pk");
await explorer.click("#sidePanelOkButton"); await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `.nodeItem >> text=${databaseId}`); await explorer.click(`.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")'); await explorer.click('button[role="menuitem"]:has-text("Delete Container")');
await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId);
await explorer.click('[aria-label="OK"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);

View File

@ -1,25 +1,24 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Tables CRUD", async () => { test("Tables CRUD", async () => {
const tableId = generateUniqueName("table"); const tableId = generateUniqueName("table");
page.setDefaultTimeout(50000);
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
await page.waitForSelector("iframe"); const explorer = await waitForExplorer();
const explorer = page.frame({
name: "explorer",
});
await page.waitForSelector('text="Querying databases"', { state: "detached" });
await explorer.click('[data-test="New Table"]'); await explorer.click('[data-test="New Table"]');
await explorer.fill('[aria-label="Table id"]', tableId); await explorer.fill('[aria-label="Table id"]', tableId);
await explorer.click("#sidePanelOkButton"); await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `[data-test="TablesDB"]`); await explorer.click(`[data-test="TablesDB"]`);
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); await explorer.click('button[role="menuitem"]:has-text("Delete Table")');
await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId);
await explorer.click('[aria-label="OK"]'); await explorer.click('[aria-label="OK"]');
await expect(explorer).not.toHaveText(".dataResourceTree", tableId); await expect(explorer).not.toHaveText(".dataResourceTree", tableId);

View File

@ -1,11 +0,0 @@
import { Frame } from "playwright";
export async function safeClick(page: Frame, selector: string): Promise<void> {
// TODO: Remove. Playwright does this for you... mostly.
// But our knockout+react setup sometimes leaves dom nodes detached and even playwright can't recover.
// Resource tree is particually bad.
// Ideally this should only be added as a last resort
await page.waitForSelector(selector);
await page.waitForTimeout(5000);
await page.click(selector);
}

View File

@ -0,0 +1,9 @@
import { Frame } from "playwright";
export const waitForExplorer = async (): Promise<Frame> => {
await page.waitForSelector("iframe");
await page.waitForTimeout(5000);
return page.frame({
name: "explorer",
});
};

View File

@ -26,7 +26,6 @@
"./src/Common/ObjectCache.ts", "./src/Common/ObjectCache.ts",
"./src/Common/OfferUtility.test.ts", "./src/Common/OfferUtility.test.ts",
"./src/Common/OfferUtility.ts", "./src/Common/OfferUtility.ts",
"./src/Common/ResourceTree.tsx",
"./src/Common/Splitter.ts", "./src/Common/Splitter.ts",
"./src/Common/ThemeUtility.ts", "./src/Common/ThemeUtility.ts",
"./src/Common/UrlUtility.ts", "./src/Common/UrlUtility.ts",
@ -38,7 +37,6 @@
"./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SelfServeContracts.ts",
"./src/Contracts/SubscriptionType.ts", "./src/Contracts/SubscriptionType.ts",
"./src/Contracts/Versions.ts", "./src/Contracts/Versions.ts",
"./src/Explorer/Controls/Dialog.tsx",
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
"./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts",
"./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts",
@ -84,7 +82,6 @@
"./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/Explorer/Tree/AccessibleVerticalList.ts",
"./src/GitHub/GitHubConnector.ts", "./src/GitHub/GitHubConnector.ts",
"./src/HostedExplorerChildFrame.ts", "./src/HostedExplorerChildFrame.ts",
"./src/Index.ts",
"./src/Platform/Hosted/Authorization.ts", "./src/Platform/Hosted/Authorization.ts",
"./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.test.tsx",
"./src/Platform/Hosted/Components/MeControl.tsx", "./src/Platform/Hosted/Components/MeControl.tsx",
@ -142,7 +139,7 @@
"./src/userContext.test.ts", "./src/userContext.test.ts",
"src/Common/EntityValue.tsx", "src/Common/EntityValue.tsx",
"./src/Platform/Hosted/Components/SwitchAccount.tsx", "./src/Platform/Hosted/Components/SwitchAccount.tsx",
"./src/Platform/Hosted/Components/SwitchSubscription.tsx", "./src/Platform/Hosted/Components/SwitchSubscription.tsx"
], ],
"include": [ "include": [
"src/CellOutputViewer/transforms/**/*", "src/CellOutputViewer/transforms/**/*",