Migrate resource tree to react (#941)
This commit is contained in:
parent
afacde4041
commit
6d46e48490
15
.env.example
15
.env.example
|
@ -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
|
|
@ -192,3 +192,4 @@ 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/ResourceTreeAdapterForResourceToken.test.tsx
|
||||||
|
src/Explorer/Tree/ResourceTree.tsx
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -2,17 +2,21 @@ 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 { 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 */}
|
||||||
|
@ -49,8 +53,10 @@ export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
|
||||||
</div>
|
</div>
|
||||||
{userContext.authType === AuthType.ResourceToken ? (
|
{userContext.authType === AuthType.ResourceToken ? (
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
|
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
|
||||||
) : (
|
) : 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 */}
|
|
@ -42,15 +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],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||||
|
@ -122,15 +113,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],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||||
|
|
|
@ -362,6 +362,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;
|
||||||
|
@ -842,6 +845,8 @@ 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);
|
||||||
|
@ -932,14 +937,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) {
|
||||||
|
|
|
@ -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));
|
||||||
|
@ -55,18 +66,20 @@ export class NotebookContentClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteContentItem(item: NotebookContentItem): Promise<void> {
|
public async deleteContentItem(item: NotebookContentItem): Promise<void> {
|
||||||
return this.deleteNotebookFile(item.path).then((path: string) => {
|
const path = await this.deleteNotebookFile(item.path);
|
||||||
if (!path || path !== item.path) {
|
useNotebook.getState().deleteNotebookItem(item);
|
||||||
throw new Error("No path provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.parent && item.parent.children) {
|
// TODO: Delete once old resource tree is removed
|
||||||
// Remove deleted child
|
if (!path || path !== item.path) {
|
||||||
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
throw new Error("No path provided");
|
||||||
item.parent.children = newChildren;
|
}
|
||||||
}
|
|
||||||
});
|
if (item.parent && item.parent.children) {
|
||||||
|
// Remove deleted child
|
||||||
|
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
||||||
|
item.parent.children = newChildren;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,12 @@ 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 { 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 { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
|
import NotebookManager from "./NotebookManager";
|
||||||
|
|
||||||
interface NotebookState {
|
interface NotebookState {
|
||||||
isNotebookEnabled: boolean;
|
isNotebookEnabled: boolean;
|
||||||
|
@ -18,6 +23,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 +35,13 @@ 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;
|
||||||
|
updateNotebookItem: (item: NotebookContentItem) => void;
|
||||||
|
deleteNotebookItem: (item: NotebookContentItem) => void;
|
||||||
|
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<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 +58,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 +118,92 @@ 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;
|
||||||
|
},
|
||||||
|
updateNotebookItem: (item: NotebookContentItem): void => {
|
||||||
|
const root = 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;
|
||||||
|
set({ myNotebooksContentRoot: root });
|
||||||
|
},
|
||||||
|
deleteNotebookItem: (item: NotebookContentItem): void => {
|
||||||
|
const root = cloneDeep(get().myNotebooksContentRoot);
|
||||||
|
const parentItem = get().findItem(root, item.parent);
|
||||||
|
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||||
|
set({ myNotebooksContentRoot: root });
|
||||||
|
},
|
||||||
|
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||||
|
set({
|
||||||
|
myNotebooksContentRoot: {
|
||||||
|
name: "My Notebooks",
|
||||||
|
path: get().notebookBasePath,
|
||||||
|
type: NotebookContentItemType.Directory,
|
||||||
|
},
|
||||||
|
galleryContentRoot: {
|
||||||
|
name: "Gallery",
|
||||||
|
path: "Gallery",
|
||||||
|
type: NotebookContentItemType.File,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notebookManager?.gitHubOAuthService?.isLoggedIn()) {
|
||||||
|
set({
|
||||||
|
gitHubNotebooksContentRoot: {
|
||||||
|
name: "GitHub repos",
|
||||||
|
path: "PsuedoDir",
|
||||||
|
type: NotebookContentItemType.Directory,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get().notebookServerInfo?.notebookServerEndpoint) {
|
||||||
|
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren({
|
||||||
|
name: "My Notebooks",
|
||||||
|
path: get().notebookBasePath,
|
||||||
|
type: NotebookContentItemType.Directory,
|
||||||
|
});
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -31,15 +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],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||||
|
|
|
@ -21,15 +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],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||||
|
|
|
@ -571,8 +571,8 @@ export default class Collection implements ViewModels.Collection {
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSettingsClick = async (): Promise<void> => {
|
public onSettingsClick = async (): Promise<void> => {
|
||||||
await this.loadOffer();
|
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
|
await this.loadOffer();
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
description: "Settings node",
|
description: "Settings node",
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -0,0 +1,722 @@
|
||||||
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
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 { 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();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = (
|
||||||
|
container: Explorer,
|
||||||
|
item: NotebookContentItem,
|
||||||
|
onFileClick: (item: NotebookContentItem) => void
|
||||||
|
): TreeNode[] => {
|
||||||
|
if (!item || !item.children) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return item.children.map((item) => {
|
||||||
|
const result =
|
||||||
|
item.type === NotebookContentItemType.Directory
|
||||||
|
? buildNotebookDirectoryNode(item, onFileClick)
|
||||||
|
: buildNotebookFileNode(item, onFileClick);
|
||||||
|
result.timestamp = item.timestamp;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebookFileNode = (
|
||||||
|
item: NotebookContentItem,
|
||||||
|
onFileClick: (item: NotebookContentItem) => void
|
||||||
|
): 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),
|
||||||
|
data: item,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => {
|
||||||
|
let items: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => container.renameNotebook(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
iconSrc: DeleteIcon,
|
||||||
|
onClick: () => {
|
||||||
|
container.showOkCancelModalDialog(
|
||||||
|
"Confirm delete",
|
||||||
|
`Are you sure you want to delete "${item.name}"`,
|
||||||
|
"Delete",
|
||||||
|
() => container.deleteNotebookFile(item),
|
||||||
|
"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): TreeNodeMenuItem[] => {
|
||||||
|
let items: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Refresh",
|
||||||
|
iconSrc: RefreshIcon,
|
||||||
|
onClick: () => loadSubitems(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
iconSrc: DeleteIcon,
|
||||||
|
onClick: () => {
|
||||||
|
container.showOkCancelModalDialog(
|
||||||
|
"Confirm delete",
|
||||||
|
`Are you sure you want to delete "${item.name}?"`,
|
||||||
|
"Delete",
|
||||||
|
() => container.deleteNotebookFile(item),
|
||||||
|
"Cancel",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
iconSrc: NotebookIcon,
|
||||||
|
onClick: () => container.renameNotebook(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Directory",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.onCreateDirectory(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Notebook",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.onNewNotebookClicked(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
iconSrc: undefined,
|
||||||
|
className: "notebookHeader",
|
||||||
|
isAlphaSorted: true,
|
||||||
|
isLeavesParentsSeparate: true,
|
||||||
|
onClick: () => {
|
||||||
|
if (!item.children) {
|
||||||
|
loadSubitems(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: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined,
|
||||||
|
data: item,
|
||||||
|
children: buildChildNodes(container, item, onFileClick),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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): Promise<void> => {
|
||||||
|
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
|
||||||
|
updateNotebookItem(updatedItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
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} />;
|
||||||
|
};
|
|
@ -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";
|
||||||
|
@ -56,8 +55,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 +71,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 +634,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}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -15,6 +15,7 @@ export type Features = {
|
||||||
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 +57,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"),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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";
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
|
@ -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}`, { timeout: 50000 });
|
||||||
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"]`);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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";
|
||||||
jest.setTimeout(240000);
|
jest.setTimeout(240000);
|
||||||
|
|
||||||
|
@ -20,11 +19,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}`, { timeout: 50000 });
|
||||||
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"]`);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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";
|
||||||
jest.setTimeout(240000);
|
jest.setTimeout(240000);
|
||||||
|
|
||||||
|
@ -20,10 +19,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}`, { timeout: 50000 });
|
||||||
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 +33,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"]`);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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";
|
||||||
jest.setTimeout(240000);
|
jest.setTimeout(240000);
|
||||||
|
|
||||||
|
@ -20,11 +19,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}`, { timeout: 50000 });
|
||||||
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"]`);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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";
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
|
@ -19,9 +18,9 @@ test("SQL CRUD", async () => {
|
||||||
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}`, { timeout: 50000 });
|
||||||
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"]`);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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";
|
||||||
|
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
@ -17,9 +16,9 @@ test("Tables CRUD", async () => {
|
||||||
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"]`, { timeout: 50000 });
|
||||||
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);
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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",
|
||||||
|
@ -142,7 +141,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/**/*",
|
||||||
|
|
Loading…
Reference in New Issue