mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-21 01:41:31 +00:00
Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migration/move-document-tab-to-react
This commit is contained in:
@@ -109,7 +109,7 @@ export const createCollectionContextMenuButton = (
|
||||
iconSrc: AddUdfIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined);
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
label: "New UDF",
|
||||
});
|
||||
|
||||
@@ -8,7 +8,9 @@ import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
|
||||
export interface AccordionComponentProps {}
|
||||
export interface AccordionComponentProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export class AccordionComponent extends React.Component<AccordionComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
@@ -78,7 +80,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
||||
);
|
||||
}
|
||||
|
||||
private onHeaderClick = (_event: React.MouseEvent<HTMLDivElement>): void => {
|
||||
private onHeaderClick = (): void => {
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
};
|
||||
|
||||
|
||||
@@ -121,8 +121,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
|
||||
if (!this.dropdownElt || !this.expandButtonElt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdownElt = $(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||
}
|
||||
|
||||
private onKeyPress(event: React.KeyboardEvent): boolean {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||
queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => {
|
||||
queryEditorModel.onDidChangeContent(() => {
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.props.onContentChanged(queryEditorModel.getValue());
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"paneMainContent"}>{content}</div>
|
||||
<div>{content}</div>
|
||||
{!this.props.showAuthorizeAccess && (
|
||||
<>
|
||||
<div className={"paneFooter"} style={ContentFooterStyle}>
|
||||
|
||||
@@ -1,154 +1,90 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { NotebookTerminalComponent } from "./NotebookTerminalComponent";
|
||||
import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent";
|
||||
|
||||
const createTestDatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
},
|
||||
type: "testType",
|
||||
};
|
||||
const testAccount: DataModels.DatabaseAccount = {
|
||||
id: "id",
|
||||
kind: "kind",
|
||||
location: "location",
|
||||
name: "name",
|
||||
properties: {
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
},
|
||||
type: "type",
|
||||
};
|
||||
|
||||
const createTestMongo32DatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
},
|
||||
type: "testType",
|
||||
};
|
||||
const testMongo32Account: DataModels.DatabaseAccount = {
|
||||
...testAccount,
|
||||
};
|
||||
|
||||
const createTestMongo36DatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
|
||||
},
|
||||
type: "testType",
|
||||
};
|
||||
const testMongo36Account: DataModels.DatabaseAccount = {
|
||||
...testAccount,
|
||||
properties: {
|
||||
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
|
||||
},
|
||||
};
|
||||
|
||||
const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
|
||||
documentEndpoint: null,
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
},
|
||||
type: "testType",
|
||||
};
|
||||
const testCassandraAccount: DataModels.DatabaseAccount = {
|
||||
...testAccount,
|
||||
properties: {
|
||||
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
|
||||
},
|
||||
};
|
||||
|
||||
const createTerminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/",
|
||||
},
|
||||
databaseAccount: createTestDatabaseAccount(),
|
||||
});
|
||||
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
||||
};
|
||||
|
||||
const createMongo32Terminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
},
|
||||
databaseAccount: createTestMongo32DatabaseAccount(),
|
||||
});
|
||||
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
};
|
||||
|
||||
const createMongo36Terminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
},
|
||||
databaseAccount: createTestMongo36DatabaseAccount(),
|
||||
});
|
||||
};
|
||||
|
||||
const createCassandraTerminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||
},
|
||||
databaseAccount: createTestCassandraDatabaseAccount(),
|
||||
});
|
||||
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: "authToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||
};
|
||||
|
||||
describe("NotebookTerminalComponent", () => {
|
||||
it("getTerminalParams: Test for terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
it("renders terminal", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testAccount,
|
||||
notebookServerInfo: testNotebookServerInfo,
|
||||
};
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([["terminal", "true"]])
|
||||
);
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Mongo 3.2 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo32Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
it("renders mongo 3.2 shell", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo32Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
};
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host],
|
||||
])
|
||||
);
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Mongo 3.6 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo36Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
it("renders mongo 3.6 shell", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo36Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
};
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host],
|
||||
])
|
||||
);
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Cassandra terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createCassandraTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
it("renders cassandra shell", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testCassandraAccount,
|
||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||
};
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host],
|
||||
])
|
||||
);
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* Wrapper around Notebook server terminal
|
||||
*/
|
||||
|
||||
import postRobot from "post-robot";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as StringUtils from "../../../Utils/StringUtils";
|
||||
import { TerminalProps } from "../../../Terminal/TerminalProps";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { TerminalQueryParams } from "../../../Common/Constants";
|
||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import * as StringUtils from "../../../Utils/StringUtils";
|
||||
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
@@ -15,79 +15,69 @@ export interface NotebookTerminalComponentProps {
|
||||
}
|
||||
|
||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||
private terminalWindow: Window;
|
||||
|
||||
constructor(props: NotebookTerminalComponentProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.sendPropsToTerminalFrame();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="notebookTerminalContainer">
|
||||
<iframe
|
||||
title="Terminal to Notebook Server"
|
||||
src={NotebookTerminalComponent.createNotebookAppSrc(this.props.notebookServerInfo, this.getTerminalParams())}
|
||||
onLoad={(event) => this.handleFrameLoad(event)}
|
||||
src="terminal.html"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public getTerminalParams(): Map<string, string> {
|
||||
let params: Map<string, string> = new Map<string, string>();
|
||||
params.set(TerminalQueryParams.Terminal, "true");
|
||||
|
||||
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
|
||||
if (terminalEndpoint) {
|
||||
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
||||
}
|
||||
|
||||
return params;
|
||||
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
||||
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
|
||||
this.sendPropsToTerminalFrame();
|
||||
}
|
||||
|
||||
public tryGetTerminalEndpoint(): string | null {
|
||||
let terminalEndpoint: string | null;
|
||||
sendPropsToTerminalFrame(): void {
|
||||
if (!this.terminalWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notebookServerEndpoint: string = this.props.notebookServerInfo.notebookServerEndpoint;
|
||||
const props: TerminalProps = {
|
||||
terminalEndpoint: this.tryGetTerminalEndpoint(),
|
||||
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
|
||||
authToken: this.props.notebookServerInfo?.authToken,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
apiType: userContext.apiType,
|
||||
authType: userContext.authType,
|
||||
databaseAccount: userContext.databaseAccount,
|
||||
};
|
||||
|
||||
postRobot.send(this.terminalWindow, "props", props, {
|
||||
domain: window.location.origin,
|
||||
});
|
||||
}
|
||||
|
||||
public tryGetTerminalEndpoint(): string | undefined {
|
||||
let terminalEndpoint: string | undefined;
|
||||
|
||||
const notebookServerEndpoint = this.props.notebookServerInfo?.notebookServerEndpoint;
|
||||
if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) {
|
||||
let mongoShellEndpoint: string = this.props.databaseAccount.properties.mongoEndpoint;
|
||||
if (!mongoShellEndpoint) {
|
||||
// mongoEndpoint is only available for Mongo 3.6 and higher.
|
||||
// Fallback to documentEndpoint otherwise.
|
||||
mongoShellEndpoint = this.props.databaseAccount.properties.documentEndpoint;
|
||||
}
|
||||
terminalEndpoint = mongoShellEndpoint;
|
||||
// mongoEndpoint is only available for Mongo 3.6 and higher, fallback to documentEndpoint otherwise
|
||||
terminalEndpoint =
|
||||
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint;
|
||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
|
||||
terminalEndpoint = this.props.databaseAccount.properties.cassandraEndpoint;
|
||||
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
||||
}
|
||||
|
||||
if (terminalEndpoint) {
|
||||
return new URL(terminalEndpoint).host;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static createNotebookAppSrc(
|
||||
serverInfo: DataModels.NotebookWorkspaceConnectionInfo,
|
||||
params: Map<string, string>
|
||||
): string {
|
||||
if (!serverInfo.notebookServerEndpoint) {
|
||||
handleError(
|
||||
"Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.",
|
||||
"NotebookTerminalComponent/createNotebookAppSrc"
|
||||
);
|
||||
return "";
|
||||
}
|
||||
|
||||
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
|
||||
if (serverInfo.authToken && serverInfo.authToken.length > 0) {
|
||||
params.set(TerminalQueryParams.Token, serverInfo.authToken);
|
||||
}
|
||||
|
||||
params.set(TerminalQueryParams.SubscriptionId, userContext.subscriptionId);
|
||||
|
||||
let result: string = "terminal.html?";
|
||||
for (let key of params.keys()) {
|
||||
result += `${key}=${encodeURIComponent(params.get(key))}&`;
|
||||
}
|
||||
|
||||
return result;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NotebookTerminalComponent renders cassandra shell 1`] = `
|
||||
<div
|
||||
className="notebookTerminalContainer"
|
||||
>
|
||||
<iframe
|
||||
onLoad={[Function]}
|
||||
src="terminal.html"
|
||||
title="Terminal to Notebook Server"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`NotebookTerminalComponent renders mongo 3.2 shell 1`] = `
|
||||
<div
|
||||
className="notebookTerminalContainer"
|
||||
>
|
||||
<iframe
|
||||
onLoad={[Function]}
|
||||
src="terminal.html"
|
||||
title="Terminal to Notebook Server"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`NotebookTerminalComponent renders mongo 3.6 shell 1`] = `
|
||||
<div
|
||||
className="notebookTerminalContainer"
|
||||
>
|
||||
<iframe
|
||||
onLoad={[Function]}
|
||||
src="terminal.html"
|
||||
title="Terminal to Notebook Server"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`NotebookTerminalComponent renders terminal 1`] = `
|
||||
<div
|
||||
className="notebookTerminalContainer"
|
||||
>
|
||||
<iframe
|
||||
onLoad={[Function]}
|
||||
src="terminal.html"
|
||||
title="Terminal to Notebook Server"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -44,10 +44,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
@@ -115,10 +111,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
|
||||
@@ -11,18 +11,17 @@ import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import {
|
||||
get as getWorkspace,
|
||||
@@ -35,7 +34,7 @@ import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@@ -47,12 +46,8 @@ import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
@@ -64,14 +59,11 @@ import TerminalTab from "./Tabs/TerminalTab";
|
||||
import Database from "./Tree/Database";
|
||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
||||
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import { useDatabases } from "./useDatabases";
|
||||
import { useSelectedNode } from "./useSelectedNode";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
var tmp = ComponentRegisterer;
|
||||
|
||||
export default class Explorer {
|
||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||
@@ -81,9 +73,6 @@ export default class Explorer {
|
||||
// Resource Tree
|
||||
private resourceTree: ResourceTreeAdapter;
|
||||
|
||||
// Resource Token
|
||||
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
|
||||
|
||||
// Tabs
|
||||
public isTabsContentExpanded: ko.Observable<boolean>;
|
||||
|
||||
@@ -143,13 +132,13 @@ export default class Explorer {
|
||||
|
||||
document.addEventListener(
|
||||
"contextmenu",
|
||||
function (e) {
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
$(document.body).click(() => $(".commandDropdownContainer").hide());
|
||||
});
|
||||
|
||||
@@ -160,6 +149,7 @@ export default class Explorer {
|
||||
case "Cassandra":
|
||||
this.tableDataClient = new CassandraAPIDataClient();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
this._initSettings();
|
||||
@@ -192,7 +182,6 @@ export default class Explorer {
|
||||
);
|
||||
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
|
||||
|
||||
// Override notebook server parameters from URL parameters
|
||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||
@@ -219,10 +208,6 @@ export default class Explorer {
|
||||
});
|
||||
}
|
||||
|
||||
if (configContext.enableSchemaAnalyzer) {
|
||||
userContext.features.enableSchemaAnalyzer = true;
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
@@ -330,19 +315,15 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
public onRefreshDatabasesKeyPress = (source: string, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
|
||||
this.onRefreshResourcesClick(source, null);
|
||||
this.onRefreshResourcesClick();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public onRefreshResourcesClick = (source: any, event: MouseEvent): void => {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
|
||||
description: "Refresh button clicked",
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
public onRefreshResourcesClick = (): void => {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
@@ -350,7 +331,7 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
// Facade
|
||||
public provideFeedbackEmail = () => {
|
||||
public provideFeedbackEmail = (): void => {
|
||||
window.open(Constants.Urls.feedbackEmail, "_blank");
|
||||
};
|
||||
|
||||
@@ -376,6 +357,9 @@ export default class Explorer {
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||
});
|
||||
|
||||
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||
|
||||
this.refreshNotebookList();
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
@@ -564,7 +548,7 @@ export default class Explorer {
|
||||
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
|
||||
promise
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason));
|
||||
.catch((reason) => this.showOkModalDialog("Unable to upload file", reason));
|
||||
return promise;
|
||||
}
|
||||
|
||||
@@ -710,13 +694,13 @@ export default class Explorer {
|
||||
const options: NotebookTabOptions = {
|
||||
account: userContext.databaseAccount,
|
||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||
node: null,
|
||||
node: undefined,
|
||||
title: notebookContentItem.name,
|
||||
tabPath: notebookContentItem.path,
|
||||
collection: null,
|
||||
collection: undefined,
|
||||
masterKey: userContext.masterKey || "",
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onLoadStartKey: undefined,
|
||||
container: this,
|
||||
notebookContentItem,
|
||||
};
|
||||
@@ -843,7 +827,7 @@ export default class Explorer {
|
||||
|
||||
clearMessage();
|
||||
},
|
||||
(error: any) => {
|
||||
(error) => {
|
||||
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
|
||||
clearMessage();
|
||||
}
|
||||
@@ -856,6 +840,8 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
await this.resourceTree.initialize();
|
||||
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||
|
||||
this.notebookManager?.refreshPinnedRepos();
|
||||
if (this.notebookToImport) {
|
||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||
@@ -895,7 +881,7 @@ export default class Explorer {
|
||||
|
||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item).then(
|
||||
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
|
||||
(reason: any) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
|
||||
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -930,7 +916,7 @@ export default class Explorer {
|
||||
return this.openNotebook(newFile);
|
||||
})
|
||||
.then(() => this.resourceTree.triggerRender())
|
||||
.catch((error: any) => {
|
||||
.catch((error) => {
|
||||
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
|
||||
logConsoleError(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
@@ -946,14 +932,15 @@ export default class Explorer {
|
||||
.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) {
|
||||
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
||||
handleError(error, "Explorer/refreshContentItem");
|
||||
return Promise.reject(new Error(error));
|
||||
}
|
||||
|
||||
return this.notebookManager?.notebookContentClient.updateItemChildren(item);
|
||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||
}
|
||||
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
||||
@@ -988,12 +975,12 @@ export default class Explorer {
|
||||
const newTab = new TerminalTab({
|
||||
account: userContext.databaseAccount,
|
||||
tabKind: ViewModels.CollectionTabKind.Terminal,
|
||||
node: null,
|
||||
node: undefined,
|
||||
title: `${title} ${index}`,
|
||||
tabPath: `${title} ${index}`,
|
||||
collection: null,
|
||||
collection: undefined,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
onLoadStartKey: undefined,
|
||||
container: this,
|
||||
kind: kind,
|
||||
index: index,
|
||||
@@ -1013,7 +1000,7 @@ export default class Explorer {
|
||||
const galleryTab = useTabs
|
||||
.getState()
|
||||
.getTabs(ViewModels.CollectionTabKind.Gallery)
|
||||
.find((tab) => tab.tabTitle() == title);
|
||||
.find((tab) => tab.tabTitle() === title);
|
||||
|
||||
if (galleryTab instanceof GalleryTab) {
|
||||
useTabs.getState().activateTab(galleryTab);
|
||||
@@ -1024,7 +1011,7 @@ export default class Explorer {
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
title,
|
||||
tabPath: title,
|
||||
onLoadStartKey: null,
|
||||
onLoadStartKey: undefined,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
},
|
||||
{
|
||||
@@ -1070,8 +1057,9 @@ export default class Explorer {
|
||||
const title = "Enable Notebooks (Preview)";
|
||||
const description =
|
||||
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
|
||||
this.openSetupNotebooksPanel(title, description);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
||||
}
|
||||
|
||||
public async handleOpenFileAction(path: string): Promise<void> {
|
||||
@@ -1106,18 +1094,6 @@ export default class Explorer {
|
||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||
}
|
||||
|
||||
public openAddDatabasePane(): void {
|
||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this} />);
|
||||
}
|
||||
|
||||
public openBrowseQueriesPanel(): void {
|
||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} />);
|
||||
}
|
||||
|
||||
public openSaveQueryPanel(): void {
|
||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this} />);
|
||||
}
|
||||
|
||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
useSidePanel
|
||||
@@ -1128,25 +1104,6 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
|
||||
public openGitHubReposPanel(header: string, junoClient?: JunoClient): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
header,
|
||||
<GitHubReposPanel
|
||||
explorer={this}
|
||||
gitHubClientProp={this.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public openSetupNotebooksPanel(title: string, description: string): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
||||
}
|
||||
|
||||
public async refreshExplorer(): Promise<void> {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { GraphHighlightedNodeData, EditedProperties } from "./GraphExplorer";
|
||||
|
||||
import React from "react";
|
||||
import { EditorNodePropertiesComponent, EditorNodePropertiesComponentProps } from "./EditorNodePropertiesComponent";
|
||||
|
||||
describe("<EditorNodePropertiesComponent />", () => {
|
||||
// Tests that: single value prop is rendered with a textbox and a delete button
|
||||
// multi-value prop only a delete button (cannot be edited)
|
||||
const onUpdateProperties = jest.fn();
|
||||
it("renders component", () => {
|
||||
const props: EditorNodePropertiesComponentProps = {
|
||||
editedProperties: {
|
||||
@@ -24,7 +23,6 @@ describe("<EditorNodePropertiesComponent />", () => {
|
||||
{ value: true, type: "boolean" },
|
||||
{ value: false, type: "boolean" },
|
||||
{ value: undefined, type: "null" },
|
||||
{ value: null, type: "null" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -41,14 +39,13 @@ describe("<EditorNodePropertiesComponent />", () => {
|
||||
{ value: true, type: "boolean" },
|
||||
{ value: false, type: "boolean" },
|
||||
{ value: undefined, type: "null" },
|
||||
{ value: null, type: "null" },
|
||||
],
|
||||
},
|
||||
],
|
||||
addedProperties: [],
|
||||
droppedKeys: [],
|
||||
},
|
||||
onUpdateProperties: (editedProperties: EditedProperties): void => {},
|
||||
onUpdateProperties,
|
||||
};
|
||||
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -81,7 +78,7 @@ describe("<EditorNodePropertiesComponent />", () => {
|
||||
addedProperties: [],
|
||||
droppedKeys: [],
|
||||
},
|
||||
onUpdateProperties: (editedProperties: EditedProperties): void => {},
|
||||
onUpdateProperties,
|
||||
};
|
||||
const wrapper = shallow(<EditorNodePropertiesComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { EditedProperties } from "./GraphExplorer";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import AddIcon from "../../../../images/Add-property.svg";
|
||||
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
import { EditedProperties } from "./GraphExplorer";
|
||||
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
|
||||
|
||||
export interface EditorNodePropertiesComponentProps {
|
||||
editedProperties: EditedProperties;
|
||||
@@ -48,7 +48,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
const editedProperties = this.props.editedProperties;
|
||||
// search for it
|
||||
for (let i = 0; i < editedProperties.existingProperties.length; i++) {
|
||||
let ip = editedProperties.existingProperties[i];
|
||||
const ip = editedProperties.existingProperties[i];
|
||||
if (ip.key === key) {
|
||||
editedProperties.existingProperties.splice(i, 1);
|
||||
editedProperties.droppedKeys.push(key);
|
||||
@@ -60,7 +60,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
|
||||
private removeAddedProperty(index: number): void {
|
||||
const editedProperties = this.props.editedProperties;
|
||||
let ap = editedProperties.addedProperties;
|
||||
const ap = editedProperties.addedProperties;
|
||||
ap.splice(index, 1);
|
||||
|
||||
this.props.onUpdateProperties(editedProperties);
|
||||
@@ -68,7 +68,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
|
||||
private addProperty(): void {
|
||||
const editedProperties = this.props.editedProperties;
|
||||
let ap = editedProperties.addedProperties;
|
||||
const ap = editedProperties.addedProperties;
|
||||
ap.push({ key: "", values: [{ value: "", type: EditorNodePropertiesComponent.DEFAULT_PROPERTY_TYPE }] });
|
||||
this.props.onUpdateProperties(editedProperties);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
onChange={(e) => {
|
||||
singleValue.type = e.target.value as ViewModels.InputPropertyValueTypeString;
|
||||
if (singleValue.type === "null") {
|
||||
singleValue.value = null;
|
||||
singleValue.value = undefined;
|
||||
}
|
||||
this.props.onUpdateProperties(this.props.editedProperties);
|
||||
}}
|
||||
@@ -144,7 +144,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
className="rightPaneTrashIcon rightPaneBtns"
|
||||
as="span"
|
||||
aria-label="Delete property"
|
||||
onActivated={(e) => this.removeExistingProperty(key)}
|
||||
onActivated={() => this.removeExistingProperty(key)}
|
||||
>
|
||||
<img src={DeleteIcon} alt="Delete" />
|
||||
</AccessibleElement>
|
||||
@@ -166,7 +166,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
className="rightPaneTrashIcon rightPaneBtns"
|
||||
as="span"
|
||||
aria-label="Remove existing property"
|
||||
onActivated={(e) => this.removeExistingProperty(nodeProp.key)}
|
||||
onActivated={() => this.removeExistingProperty(nodeProp.key)}
|
||||
>
|
||||
<img src={DeleteIcon} alt="Delete" />
|
||||
</AccessibleElement>
|
||||
@@ -206,7 +206,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
onChange={(e) => {
|
||||
firstValue.value = e.target.value;
|
||||
if (firstValue.type === "null") {
|
||||
firstValue.value = null;
|
||||
firstValue.value = undefined;
|
||||
}
|
||||
this.props.onUpdateProperties(this.props.editedProperties);
|
||||
}}
|
||||
@@ -235,7 +235,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
|
||||
className="rightPaneTrashIcon rightPaneBtns"
|
||||
as="span"
|
||||
aria-label="Remove property"
|
||||
onActivated={(e) => this.removeAddedProperty(index)}
|
||||
onActivated={() => this.removeAddedProperty(index)}
|
||||
>
|
||||
<img src={DeleteIcon} alt="Delete" />
|
||||
</AccessibleElement>
|
||||
|
||||
@@ -82,7 +82,7 @@ export class QueryContainerComponent extends React.Component<
|
||||
<button
|
||||
type="button"
|
||||
className="filterbtnstyle queryButton"
|
||||
onClick={(e) => this.props.onExecuteClick(this.state.query)}
|
||||
onClick={() => this.props.onExecuteClick(this.state.query)}
|
||||
disabled={this.props.isLoading || !QueryContainerComponent.isQueryValid(this.state.query)}
|
||||
>
|
||||
Execute Gremlin Query
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
import { GraphHighlightedNodeData, NeighborVertexBasicInfo } from "./GraphExplorer";
|
||||
import * as GraphUtil from "./GraphUtil";
|
||||
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
||||
|
||||
export interface ReadOnlyNeighborsComponentProps {
|
||||
node: GraphHighlightedNodeData;
|
||||
@@ -48,7 +48,7 @@ export class ReadOnlyNeighborsComponent extends React.Component<ReadOnlyNeighbor
|
||||
className="clickableLink"
|
||||
as="a"
|
||||
aria-label={_neighbor.name}
|
||||
onActivated={(e) => this.props.selectNode(_neighbor.id)}
|
||||
onActivated={() => this.props.selectNode(_neighbor.id)}
|
||||
title={GraphUtil.getNeighborTitle(_neighbor)}
|
||||
>
|
||||
{_neighbor.name}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { GraphHighlightedNodeData } from "./GraphExplorer";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { GraphHighlightedNodeData } from "./GraphExplorer";
|
||||
|
||||
export interface ReadOnlyNodePropertiesComponentProps {
|
||||
node: GraphHighlightedNodeData;
|
||||
|
||||
@@ -37,7 +37,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
</td>
|
||||
<td
|
||||
className="valueCol"
|
||||
title="efgh, 1234, true, false, undefined, null"
|
||||
title="efgh, 1234, true, false, undefined"
|
||||
>
|
||||
<div
|
||||
className="propertyValue"
|
||||
@@ -69,12 +69,6 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
>
|
||||
undefined
|
||||
</div>
|
||||
<div
|
||||
className="propertyValue isNull"
|
||||
key="null"
|
||||
>
|
||||
null
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
@@ -178,12 +172,6 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
|
||||
>
|
||||
undefined
|
||||
</div>
|
||||
<div
|
||||
className="propertyValue isNull"
|
||||
key="null"
|
||||
>
|
||||
null
|
||||
</div>
|
||||
</td>
|
||||
<td />
|
||||
<td
|
||||
|
||||
@@ -256,7 +256,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
console.log(mockExplorer);
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
|
||||
@@ -22,6 +22,7 @@ import * as Constants from "../../../Common/Constants";
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
@@ -30,8 +31,12 @@ import { CommandButtonComponentProps } from "../../Controls/CommandButton/Comman
|
||||
import Explorer from "../../Explorer";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
|
||||
@@ -281,9 +286,8 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
return {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
container.openAddDatabasePane();
|
||||
},
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
@@ -415,7 +419,8 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
||||
return {
|
||||
iconSrc: BrowseQueriesIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openBrowseQueriesPanel(),
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
@@ -448,7 +453,13 @@ function createEnableNotebooksButton(container: Explorer): CommandButtonComponen
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openSetupNotebooksPanel(label, description),
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||
@@ -486,7 +497,12 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -513,7 +529,12 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
container.openSetupNotebooksPanel(title, description);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -540,10 +561,21 @@ function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonC
|
||||
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
|
||||
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
|
||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||
const junoClient = new JunoClient();
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openGitHubReposPanel(label),
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<GitHubReposPanel
|
||||
explorer={container}
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as NteractUtil from "../NTeractUtil";
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
|
||||
import NotebookRenderer from "../../../NotebookRenderer/NotebookRenderer";
|
||||
import * as TextFile from "./text-file";
|
||||
|
||||
@@ -32,14 +31,14 @@ interface FileProps {
|
||||
|
||||
export class File extends React.PureComponent<FileProps> {
|
||||
getChoice = () => {
|
||||
let choice = null;
|
||||
let choice;
|
||||
|
||||
// notebooks don't report a mimetype so we'll use the content.type
|
||||
if (this.props.type === "notebook") {
|
||||
choice = <NotebookRenderer contentRef={this.props.contentRef} />;
|
||||
} else if (this.props.type === "dummy") {
|
||||
choice = null;
|
||||
} else if (this.props.mimetype == null || !TextFile.handles(this.props.mimetype)) {
|
||||
choice = undefined;
|
||||
} else if (this.props.mimetype === undefined || !TextFile.handles(this.props.mimetype)) {
|
||||
// This should not happen as we intercept mimetype upstream, but just in case
|
||||
choice = (
|
||||
<PaddedContainer>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as StringUtils from "../../../../../Utils/StringUtils";
|
||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import styled from "styled-components";
|
||||
import * as StringUtils from "../../../../../Utils/StringUtils";
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
position: absolute;
|
||||
@@ -37,7 +37,7 @@ interface TextFileState {
|
||||
class EditorPlaceholder extends React.PureComponent<MonacoEditorProps> {
|
||||
render(): JSX.Element {
|
||||
// TODO: Show a little blocky placeholder
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ function makeMapStateToTextFileProps(
|
||||
|
||||
return {
|
||||
contentRef,
|
||||
mimetype: content.mimetype != null ? content.mimetype : "text/plain",
|
||||
mimetype: content.mimetype !== undefined ? content.mimetype : "text/plain",
|
||||
text,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { stringifyNotebook } from "@nteract/commutable";
|
||||
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import * as StringUtils from "../../Utils/StringUtils";
|
||||
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
|
||||
* @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) => {
|
||||
item.children = subItems;
|
||||
subItems.forEach((subItem) => (subItem.parent = item));
|
||||
@@ -55,18 +66,20 @@ export class NotebookContentClient {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteContentItem(item: NotebookContentItem): Promise<void> {
|
||||
return this.deleteNotebookFile(item.path).then((path: string) => {
|
||||
if (!path || path !== item.path) {
|
||||
throw new Error("No path provided");
|
||||
}
|
||||
public async deleteContentItem(item: NotebookContentItem): Promise<void> {
|
||||
const path = await this.deleteNotebookFile(item.path);
|
||||
useNotebook.getState().deleteNotebookItem(item);
|
||||
|
||||
if (item.parent && item.parent.children) {
|
||||
// Remove deleted child
|
||||
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
||||
item.parent.children = newChildren;
|
||||
}
|
||||
});
|
||||
// TODO: Delete once old resource tree is removed
|
||||
if (!path || path !== item.path) {
|
||||
throw new Error("No path provided");
|
||||
}
|
||||
|
||||
if (item.parent && item.parent.children) {
|
||||
// Remove deleted child
|
||||
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
||||
item.parent.children = newChildren;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ import { userContext } from "../../UserContext";
|
||||
import { getFullName } from "../../Utils/UserUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
|
||||
@@ -88,7 +89,18 @@ export default class NotebookManager {
|
||||
this.gitHubClient.setToken(token?.access_token);
|
||||
if (this?.gitHubOAuthService.isLoggedIn()) {
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
this.params.container.openGitHubReposPanel("Manager GitHub settings", this.junoClient);
|
||||
setTimeout(() => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Manage GitHub settings",
|
||||
<GitHubReposPanel
|
||||
explorer={this.params.container}
|
||||
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
|
||||
junoClientProp={this.junoClient}
|
||||
/>
|
||||
);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
this.params.refreshCommandBarButtons();
|
||||
@@ -129,6 +141,7 @@ export default class NotebookManager {
|
||||
notebookContentRef={notebookContentRef}
|
||||
onTakeSnapshot={onTakeSnapshot}
|
||||
/>,
|
||||
"440px",
|
||||
onClosePanel
|
||||
);
|
||||
}
|
||||
@@ -161,7 +174,17 @@ export default class NotebookManager {
|
||||
undefined,
|
||||
"Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.",
|
||||
"Connect to GitHub",
|
||||
() => this.params.container.openGitHubReposPanel("Connect to GitHub"),
|
||||
() =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Connect to GitHub",
|
||||
<GitHubReposPanel
|
||||
explorer={this.params.container}
|
||||
gitHubClientProp={this.params.container.notebookManager.gitHubClient}
|
||||
junoClientProp={this.junoClient}
|
||||
/>
|
||||
),
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import "./Prompt.less";
|
||||
|
||||
export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
||||
if (props.status === "busy") {
|
||||
const stopButtonText: string = "Stop cell execution";
|
||||
const stopButtonText = "Stop cell execution";
|
||||
return (
|
||||
<div
|
||||
style={{ position: "sticky", width: "100%", maxHeight: "100%", left: 0, top: 0, zIndex: 300 }}
|
||||
@@ -23,7 +23,7 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
||||
</div>
|
||||
);
|
||||
} else if (props.isHovered) {
|
||||
const playButtonText: string = "Run cell";
|
||||
const playButtonText = "Run cell";
|
||||
return (
|
||||
<IconButton
|
||||
className="runCellButton"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { StatusBar } from "./StatusBar";
|
||||
|
||||
describe("StatusBar", () => {
|
||||
@@ -28,8 +27,8 @@ describe("StatusBar", () => {
|
||||
kernelSpecDisplayName: "javascript",
|
||||
kernelStatus: "kernelStatus",
|
||||
},
|
||||
null,
|
||||
null
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(shouldUpdate).toBe(true);
|
||||
});
|
||||
@@ -47,8 +46,8 @@ describe("StatusBar", () => {
|
||||
kernelSpecDisplayName: "python3",
|
||||
kernelStatus: "kernelStatus",
|
||||
},
|
||||
null,
|
||||
null
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(shouldUpdate).toBe(true);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
|
||||
interface Props {
|
||||
@@ -12,8 +13,6 @@ interface Props {
|
||||
|
||||
const NOT_CONNECTED = "not connected";
|
||||
|
||||
import styled from "styled-components";
|
||||
|
||||
export const LeftStatus = styled.div`
|
||||
float: left;
|
||||
display: block;
|
||||
@@ -80,7 +79,7 @@ interface InitialProps {
|
||||
contentRef: ContentRef;
|
||||
}
|
||||
|
||||
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
|
||||
const makeMapStateToProps = (_initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
|
||||
const { contentRef } = initialProps;
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
@@ -90,26 +89,26 @@ const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps)
|
||||
return {
|
||||
kernelStatus: NOT_CONNECTED,
|
||||
kernelSpecDisplayName: "no kernel",
|
||||
lastSaved: null,
|
||||
lastSaved: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const kernelRef = content.model.kernelRef;
|
||||
let kernel = null;
|
||||
let kernel;
|
||||
if (kernelRef) {
|
||||
kernel = selectors.kernel(state, { kernelRef });
|
||||
}
|
||||
|
||||
const lastSaved = content && content.lastSaved ? content.lastSaved : null;
|
||||
const lastSaved = content && content.lastSaved ? content.lastSaved : undefined;
|
||||
|
||||
const kernelStatus = kernel != null && kernel.status != null ? kernel.status : NOT_CONNECTED;
|
||||
const kernelStatus = kernel?.status || NOT_CONNECTED;
|
||||
|
||||
// TODO: We need kernels associated to the kernelspec they came from
|
||||
// so we can pluck off the display_name and provide it here
|
||||
let kernelSpecDisplayName = " ";
|
||||
if (kernelStatus === NOT_CONNECTED) {
|
||||
kernelSpecDisplayName = "no kernel";
|
||||
} else if (kernel != null && kernel.kernelSpecName != null) {
|
||||
} else if (kernel?.kernelSpecName) {
|
||||
kernelSpecDisplayName = kernel.kernelSpecName;
|
||||
} else if (content && content.type === "notebook") {
|
||||
kernelSpecDisplayName = selectors.notebook.displayName(content.model) || " ";
|
||||
|
||||
@@ -27,7 +27,7 @@ interface DispatchProps {
|
||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
||||
clearOutputs: () => void;
|
||||
deleteCell: () => void;
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) => void;
|
||||
takeNotebookSnapshot: (payload: SnapshotRequest) => void;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ const mapDispatchToProps = (
|
||||
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
||||
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) =>
|
||||
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data })),
|
||||
takeNotebookSnapshot: (request: SnapshotRequest) => dispatch(cdbActions.takeNotebookSnapshot(request)),
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ContentRef } from "@nteract/core";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { ContentRef } from "@nteract/core";
|
||||
import * as actions from "../../NotebookComponent/actions";
|
||||
|
||||
interface ComponentProps {
|
||||
@@ -29,10 +28,7 @@ class HoverableCell extends React.Component<ComponentProps & DispatchProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: Dispatch,
|
||||
{ id, contentRef }: { id: string; contentRef: ContentRef }
|
||||
): DispatchProps => ({
|
||||
const mapDispatchToProps = (dispatch: Dispatch, { id }: { id: string }): DispatchProps => ({
|
||||
hover: () => dispatch(actions.setHoveredCell({ cellId: id })),
|
||||
unHover: () => dispatch(actions.setHoveredCell({ cellId: undefined })),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -5,8 +6,12 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
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 { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import NotebookManager from "./NotebookManager";
|
||||
|
||||
interface NotebookState {
|
||||
isNotebookEnabled: boolean;
|
||||
@@ -18,6 +23,9 @@ interface NotebookState {
|
||||
isShellEnabled: boolean;
|
||||
notebookBasePath: string;
|
||||
isInitializingNotebooks: boolean;
|
||||
myNotebooksContentRoot: NotebookContentItem;
|
||||
gitHubNotebooksContentRoot: NotebookContentItem;
|
||||
galleryContentRoot: NotebookContentItem;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
@@ -27,9 +35,13 @@ interface NotebookState {
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
||||
setNotebookBasePath: (notebookBasePath: string) => 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,
|
||||
isNotebooksEnabledForAccount: false,
|
||||
notebookServerInfo: {
|
||||
@@ -46,6 +58,9 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
||||
isShellEnabled: false,
|
||||
notebookBasePath: Constants.Notebook.defaultBasePath,
|
||||
isInitializingNotebooks: false,
|
||||
myNotebooksContentRoot: undefined,
|
||||
gitHubNotebooksContentRoot: undefined,
|
||||
galleryContentRoot: undefined,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
@@ -103,4 +118,88 @@ export const useNotebook: UseStore<NotebookState> = create((set) => ({
|
||||
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> => {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -113,7 +113,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
collectionId: "",
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: "",
|
||||
partitionKey:
|
||||
(userContext.features.partitionKeyDefault && userContext.apiType === "SQL") ||
|
||||
(userContext.features.partitionKeyDefault && userContext.apiType === "Mongo")
|
||||
? "/id"
|
||||
: "",
|
||||
enableDedicatedThroughput: false,
|
||||
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
|
||||
useHashV2: false,
|
||||
@@ -413,6 +417,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Text variant="small" aria-label="pkDescription">
|
||||
{this.getPartitionKeySubtext()}
|
||||
</Text>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="addCollection-partitionKeyValue"
|
||||
@@ -807,6 +815,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return tooltipText;
|
||||
}
|
||||
|
||||
private getPartitionKeySubtext(): string {
|
||||
if (
|
||||
userContext.features.partitionKeyDefault &&
|
||||
(userContext.apiType === "SQL" || userContext.apiType === "Mongo")
|
||||
) {
|
||||
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
|
||||
return subtext;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
|
||||
@@ -120,6 +120,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
||||
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
||||
}
|
||||
}
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
}
|
||||
|
||||
public resetData(): void {
|
||||
@@ -144,11 +145,18 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
||||
|
||||
private setup(forceShowConnectToGitHub = false): void {
|
||||
forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn()
|
||||
? this.setupForConnectToGitHub()
|
||||
? this.setupForConnectToGitHub(forceShowConnectToGitHub)
|
||||
: this.setupForManageRepos();
|
||||
}
|
||||
|
||||
private setupForConnectToGitHub(): void {
|
||||
private setupForConnectToGitHub(forceShowConnectToGitHub: boolean): void {
|
||||
if (forceShowConnectToGitHub) {
|
||||
const newState = { ...this.state.gitHubReposState };
|
||||
newState.showAuthorizeAccess = forceShowConnectToGitHub;
|
||||
this.setState({
|
||||
gitHubReposState: newState,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
isExecuting: false,
|
||||
});
|
||||
@@ -368,46 +376,28 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
||||
isLoading: true,
|
||||
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
|
||||
};
|
||||
this.setState({
|
||||
gitHubReposState: {
|
||||
...this.state.gitHubReposState,
|
||||
reposListProps: {
|
||||
...this.state.gitHubReposState.reposListProps,
|
||||
branchesProps: {
|
||||
...this.state.gitHubReposState.reposListProps.branchesProps,
|
||||
[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]: this.branchesProps[item.key],
|
||||
},
|
||||
pinnedReposProps: {
|
||||
repos: this.pinnedReposProps.repos,
|
||||
},
|
||||
unpinnedReposProps: {
|
||||
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
|
||||
repos: this.unpinnedReposProps.repos,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
this.loadMoreBranches(item.repo);
|
||||
} else {
|
||||
if (this.isAddedRepo === false) {
|
||||
this.setState({
|
||||
gitHubReposState: {
|
||||
...this.state.gitHubReposState,
|
||||
reposListProps: {
|
||||
...this.state.gitHubReposState.reposListProps,
|
||||
pinnedReposProps: {
|
||||
repos: this.pinnedReposProps.repos,
|
||||
},
|
||||
unpinnedReposProps: {
|
||||
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
|
||||
repos: this.unpinnedReposProps.repos,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
gitHubReposState: {
|
||||
...this.state.gitHubReposState,
|
||||
reposListProps: {
|
||||
...this.state.gitHubReposState.reposListProps,
|
||||
branchesProps: {
|
||||
...this.branchesProps,
|
||||
},
|
||||
pinnedReposProps: {
|
||||
repos: this.pinnedReposProps.repos,
|
||||
},
|
||||
unpinnedReposProps: {
|
||||
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
|
||||
repos: this.unpinnedReposProps.repos,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
this.isAddedRepo = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
"getRepo": [Function],
|
||||
"pinRepo": [Function],
|
||||
|
||||
@@ -150,9 +150,6 @@
|
||||
.backImageIcon {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entityValueTextField {
|
||||
margin: 24px;
|
||||
}
|
||||
.addEntityDatePicker {
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
|
||||
export interface PanelContainerProps {
|
||||
headerText: string;
|
||||
panelContent: JSX.Element;
|
||||
headerText?: string;
|
||||
panelContent?: JSX.Element;
|
||||
isConsoleExpanded: boolean;
|
||||
isOpen: boolean;
|
||||
panelWidth?: string;
|
||||
@@ -66,8 +66,8 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
);
|
||||
}
|
||||
|
||||
private onDissmiss = (ev?: React.SyntheticEvent<HTMLElement>): void => {
|
||||
if ((ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||
private onDissmiss = (ev?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
|
||||
if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||
ev.preventDefault();
|
||||
} else {
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
@@ -85,11 +85,12 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
|
||||
export const SidePanel: React.FC = () => {
|
||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||
const { isOpen, panelContent, headerText } = useSidePanel((state) => {
|
||||
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||
return {
|
||||
isOpen: state.isOpen,
|
||||
panelContent: state.panelContent,
|
||||
headerText: state.headerText,
|
||||
panelWidth: state.panelWidth,
|
||||
};
|
||||
});
|
||||
// TODO Refactor PanelContainerComponent into a functional component and remove this wrapper
|
||||
@@ -100,6 +101,7 @@ export const SidePanel: React.FC = () => {
|
||||
panelContent={panelContent}
|
||||
headerText={headerText}
|
||||
isConsoleExpanded={isConsoleExpanded}
|
||||
panelWidth={panelWidth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,10 +23,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
inProgressMessage="Creating directory "
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as _ from "underscore";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { TableEntity } from "../../../Common/TableEntity";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as TableConstants from "../../Tables/Constants";
|
||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||
@@ -15,7 +15,7 @@ import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../.
|
||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||
import * as Utilities from "../../Tables/Utilities";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import { PanelContainerComponent } from "../PanelContainerComponent";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import {
|
||||
attributeNameLabel,
|
||||
attributeValueLabel,
|
||||
@@ -30,9 +30,7 @@ import {
|
||||
getCassandraDefaultEntities,
|
||||
getDefaultEntities,
|
||||
getEntityValuePlaceholder,
|
||||
getPanelTitle,
|
||||
imageProps,
|
||||
isValidEntities,
|
||||
options,
|
||||
} from "./Validators/EntityTableHelper";
|
||||
|
||||
@@ -61,7 +59,6 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
tableEntityListViewModel,
|
||||
cassandraApiClient,
|
||||
}: AddTableEntityPanelProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
||||
const [selectedRow, setSelectedRow] = useState<number>(0);
|
||||
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
||||
@@ -70,6 +67,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
isEntityValuePanelOpen,
|
||||
{ setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse },
|
||||
] = useBoolean(false);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
|
||||
/* Get default and previous saved entity headers */
|
||||
useEffect(() => {
|
||||
@@ -98,19 +97,36 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
};
|
||||
|
||||
/* Add new entity attribute */
|
||||
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
|
||||
if (!isValidEntities(entities)) {
|
||||
return undefined;
|
||||
}
|
||||
event.preventDefault();
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const { property, type } = entities[i];
|
||||
if (property === "" || property === undefined) {
|
||||
setFormError(`Property name cannot be empty. Please enter a property name`);
|
||||
return;
|
||||
}
|
||||
|
||||
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 newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||
tableEntityListViewModel.redrawTableThrottled();
|
||||
try {
|
||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||
tableEntityListViewModel.redrawTableThrottled();
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError(errorMessage);
|
||||
handleError(errorMessage, "AddTableRow");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
||||
@@ -200,110 +216,80 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
setIsEntityValuePanelTrue();
|
||||
};
|
||||
|
||||
const renderPanelContent = (): JSX.Element => {
|
||||
return (
|
||||
<form className="panelFormWrapper">
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
{entities.map((entity, index) => {
|
||||
return (
|
||||
<TableEntity
|
||||
key={"" + entity.id + index}
|
||||
isDeleteOptionVisible={entity.isDeleteOptionVisible}
|
||||
entityTypeLabel={index === 0 && dataTypeLabel}
|
||||
entityPropertyLabel={index === 0 && attributeNameLabel}
|
||||
entityValueLabel={index === 0 && attributeValueLabel}
|
||||
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
|
||||
isPropertyTypeDisable={entity.isPropertyTypeDisable}
|
||||
entityProperty={entity.property}
|
||||
selectedKey={entity.type}
|
||||
entityPropertyPlaceHolder={detailedHelp}
|
||||
entityValuePlaceholder={entity.entityValuePlaceholder}
|
||||
entityValue={entity.value}
|
||||
isEntityTypeDate={entity.isEntityTypeDate}
|
||||
entityTimeValue={entity.entityTimeValue}
|
||||
onEditEntity={() => editEntity(index)}
|
||||
onSelectDate={(date: Date) => {
|
||||
entityChange(date, index, "value");
|
||||
}}
|
||||
onDeleteEntity={() => deleteEntityAtIndex(index)}
|
||||
onEntityPropertyChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "property");
|
||||
}}
|
||||
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
|
||||
entityTypeChange(event, selectedParam, index);
|
||||
}}
|
||||
onEntityValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "value");
|
||||
}}
|
||||
onEntityTimeValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "time");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType !== "Cassandra" && (
|
||||
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
||||
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
<div className="paneFooter">
|
||||
<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}
|
||||
/>
|
||||
<Stack style={{ padding: "20px 34px" }}>
|
||||
<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 (
|
||||
<PanelContainerComponent
|
||||
headerText={getPanelTitle(userContext.apiType)}
|
||||
panelWidth="700px"
|
||||
isOpen={true}
|
||||
panelContent={renderPanelContent()}
|
||||
isConsoleExpanded={false}
|
||||
/>
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelMainContent">
|
||||
{entities.map((entity, index) => {
|
||||
return (
|
||||
<TableEntity
|
||||
key={"" + entity.id + index}
|
||||
isDeleteOptionVisible={entity.isDeleteOptionVisible}
|
||||
entityTypeLabel={index === 0 && dataTypeLabel}
|
||||
entityPropertyLabel={index === 0 && attributeNameLabel}
|
||||
entityValueLabel={index === 0 && attributeValueLabel}
|
||||
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
|
||||
isPropertyTypeDisable={entity.isPropertyTypeDisable}
|
||||
entityProperty={entity.property}
|
||||
selectedKey={entity.type}
|
||||
entityPropertyPlaceHolder={detailedHelp}
|
||||
entityValuePlaceholder={entity.entityValuePlaceholder}
|
||||
entityValue={entity.value}
|
||||
isEntityTypeDate={entity.isEntityTypeDate}
|
||||
entityTimeValue={entity.entityTimeValue}
|
||||
onEditEntity={() => editEntity(index)}
|
||||
onSelectDate={(date: Date) => {
|
||||
entityChange(date, index, "value");
|
||||
}}
|
||||
onDeleteEntity={() => deleteEntityAtIndex(index)}
|
||||
onEntityPropertyChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "property");
|
||||
}}
|
||||
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
|
||||
entityTypeChange(event, selectedParam, index);
|
||||
}}
|
||||
onEntityValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "value");
|
||||
}}
|
||||
onEntityTimeValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "time");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType !== "Cassandra" && (
|
||||
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
||||
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IDropdownOption, Image, IPanelProps, IRenderFunction, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as _ from "underscore";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { TableEntity } from "../../../Common/TableEntity";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as TableConstants from "../../Tables/Constants";
|
||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||
@@ -14,7 +14,7 @@ import * as Entities from "../../Tables/Entities";
|
||||
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
|
||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import { PanelContainerComponent } from "../PanelContainerComponent";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import {
|
||||
attributeNameLabel,
|
||||
attributeValueLabel,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
getEntityValuePlaceholder,
|
||||
getFormattedTime,
|
||||
imageProps,
|
||||
isValidEntities,
|
||||
options,
|
||||
} from "./Validators/EntityTableHelper";
|
||||
|
||||
@@ -59,12 +58,13 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
tableEntityListViewModel,
|
||||
cassandraApiClient,
|
||||
}: EditTableEntityPanelProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
||||
const [selectedRow, setSelectedRow] = useState<number>(0);
|
||||
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
||||
const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({});
|
||||
const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>("");
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
|
||||
const [
|
||||
isEntityValuePanelOpen,
|
||||
@@ -190,26 +190,44 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
return displayValue;
|
||||
};
|
||||
|
||||
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
|
||||
if (!isValidEntities(entities)) {
|
||||
return undefined;
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const { property, type } = entities[i];
|
||||
if (property === "" || property === undefined) {
|
||||
setFormError(`Property name cannot be empty. Please enter a property name`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
setIsExecuting(true);
|
||||
const entity: Entities.ITableEntity = entityFromAttributes(entities);
|
||||
const newTableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : tableDataClient;
|
||||
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
|
||||
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
|
||||
queryTablesTab.collection,
|
||||
originalDocumentData,
|
||||
entity
|
||||
);
|
||||
await tableEntityListViewModel.updateCachedEntity(newEntity);
|
||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||
tableEntityListViewModel.redrawTableThrottled();
|
||||
|
||||
try {
|
||||
const newEntity: Entities.ITableEntity = await newTableDataClient.updateDocument(
|
||||
queryTablesTab.collection,
|
||||
originalDocumentData,
|
||||
entity
|
||||
);
|
||||
await tableEntityListViewModel.updateCachedEntity(newEntity);
|
||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
||||
tableEntityListViewModel.redrawTableThrottled();
|
||||
}
|
||||
tableEntityListViewModel.selected.removeAll();
|
||||
tableEntityListViewModel.selected.push(newEntity);
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
handleError(errorMessage, "EditTableRow");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
tableEntityListViewModel.selected.removeAll();
|
||||
tableEntityListViewModel.selected.push(newEntity);
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
||||
@@ -299,109 +317,81 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
setIsEntityValuePanelTrue();
|
||||
};
|
||||
|
||||
const renderPanelContent = (): JSX.Element => {
|
||||
return (
|
||||
<form className="panelFormWrapper">
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
{entities.map((entity, index) => {
|
||||
return (
|
||||
<TableEntity
|
||||
key={"" + entity.id + index}
|
||||
isDeleteOptionVisible={entity.isDeleteOptionVisible}
|
||||
entityTypeLabel={index === 0 && dataTypeLabel}
|
||||
entityPropertyLabel={index === 0 && attributeNameLabel}
|
||||
entityValueLabel={index === 0 && attributeValueLabel}
|
||||
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
|
||||
isPropertyTypeDisable={entity.isPropertyTypeDisable}
|
||||
entityProperty={entity.property}
|
||||
selectedKey={entity.type}
|
||||
entityPropertyPlaceHolder={detailedHelp}
|
||||
entityValuePlaceholder={entity.entityValuePlaceholder}
|
||||
entityValue={entity.value?.toString()}
|
||||
isEntityTypeDate={entity.isEntityTypeDate}
|
||||
entityTimeValue={entity.entityTimeValue}
|
||||
isEntityValueDisable={entity.isEntityValueDisable}
|
||||
onEditEntity={() => editEntity(index)}
|
||||
onSelectDate={(date: Date) => {
|
||||
entityChange(date, index, "value");
|
||||
}}
|
||||
onDeleteEntity={() => deleteEntityAtIndex(index)}
|
||||
onEntityPropertyChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "property");
|
||||
}}
|
||||
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
|
||||
entityTypeChange(event, selectedParam, index);
|
||||
}}
|
||||
onEntityValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "value");
|
||||
}}
|
||||
onEntityTimeValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "time");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType !== "Cassandra" && (
|
||||
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
||||
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
{renderPanelFooter()}
|
||||
</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}
|
||||
/>
|
||||
<Stack style={{ padding: "20px 34px" }}>
|
||||
<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 (
|
||||
<PanelContainerComponent
|
||||
headerText="Edit Table Entity"
|
||||
panelWidth="700px"
|
||||
isOpen={true}
|
||||
panelContent={renderPanelContent()}
|
||||
isConsoleExpanded={false}
|
||||
/>
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelMainContent">
|
||||
{entities.map((entity, index) => {
|
||||
return (
|
||||
<TableEntity
|
||||
key={"" + entity.id + index}
|
||||
isDeleteOptionVisible={entity.isDeleteOptionVisible}
|
||||
entityTypeLabel={index === 0 && dataTypeLabel}
|
||||
entityPropertyLabel={index === 0 && attributeNameLabel}
|
||||
entityValueLabel={index === 0 && attributeValueLabel}
|
||||
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
|
||||
isPropertyTypeDisable={entity.isPropertyTypeDisable}
|
||||
entityProperty={entity.property}
|
||||
selectedKey={entity.type}
|
||||
entityPropertyPlaceHolder={detailedHelp}
|
||||
entityValuePlaceholder={entity.entityValuePlaceholder}
|
||||
entityValue={entity.value?.toString()}
|
||||
isEntityTypeDate={entity.isEntityTypeDate}
|
||||
entityTimeValue={entity.entityTimeValue}
|
||||
isEntityValueDisable={entity.isEntityValueDisable}
|
||||
onEditEntity={() => editEntity(index)}
|
||||
onSelectDate={(date: Date) => {
|
||||
entityChange(date, index, "value");
|
||||
}}
|
||||
onDeleteEntity={() => deleteEntityAtIndex(index)}
|
||||
onEntityPropertyChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "property");
|
||||
}}
|
||||
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
|
||||
entityTypeChange(event, selectedParam, index);
|
||||
}}
|
||||
onEntityValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "value");
|
||||
}}
|
||||
onEntityTimeValueChange={(event, newInput?: string) => {
|
||||
entityChange(newInput, index, "time");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType !== "Cassandra" && (
|
||||
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
||||
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ export const int64Placeholder = "Enter a signed 64-bit integer, in the range (-2
|
||||
|
||||
export const columnProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 10 },
|
||||
styles: { root: { width: 680 } },
|
||||
styles: { root: { marginBottom: 8 } },
|
||||
};
|
||||
|
||||
// helper functions
|
||||
@@ -134,8 +134,8 @@ export const getEntityValuePlaceholder = (entityType: string | number): string =
|
||||
|
||||
export const isValidEntities = (entities: EntityRowType[]): boolean => {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const { property } = entities[i];
|
||||
if (property === "" || property === undefined) {
|
||||
const { property, type } = entities[i];
|
||||
if (property === "" || property === undefined || !type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -170,13 +170,6 @@ export const getDefaultEntities = (headers: string[], entityTypes: EntityType):
|
||||
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 => {
|
||||
if (apiType === "Cassandra") {
|
||||
return "Add Row";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../Utils/APITypeUtils";
|
||||
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
|
||||
@@ -23,6 +24,8 @@ import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
|
||||
import Explorer from "../Explorer";
|
||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { AddDatabasePanel } from "../Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { BrowseQueriesPane } from "../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
@@ -173,7 +176,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a role="link" href={SplashScreen.seeMoreItemUrl} target="_blank" tabIndex={0}>
|
||||
<a role="link" href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}>
|
||||
{SplashScreen.seeMoreItemTitle}
|
||||
</a>
|
||||
</li>
|
||||
@@ -241,20 +244,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined);
|
||||
},
|
||||
title: "New SQL Query",
|
||||
description: null,
|
||||
description: undefined,
|
||||
});
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined);
|
||||
},
|
||||
title: "New Query",
|
||||
description: null,
|
||||
description: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,8 +265,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
items.push({
|
||||
iconSrc: OpenQueryIcon,
|
||||
title: "Open Query",
|
||||
description: null,
|
||||
onClick: () => this.container.openBrowseQueriesPanel(),
|
||||
description: undefined,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.container} />),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -271,10 +277,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
items.push({
|
||||
iconSrc: NewStoredProcedureIcon,
|
||||
title: "New Stored Procedure",
|
||||
description: null,
|
||||
description: undefined,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -286,7 +292,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
items.push({
|
||||
iconSrc: ScaleAndSettingsIcon,
|
||||
title: label,
|
||||
description: null,
|
||||
description: undefined,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onSettingsClick();
|
||||
@@ -296,8 +302,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
items.push({
|
||||
iconSrc: AddDatabaseIcon,
|
||||
title: "New " + getDatabaseName(),
|
||||
description: null,
|
||||
onClick: () => this.container.openAddDatabasePane(),
|
||||
description: undefined,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -348,19 +357,19 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
private createTipsItems(): SplashScreenItem[] {
|
||||
return [
|
||||
{
|
||||
iconSrc: null,
|
||||
iconSrc: undefined,
|
||||
title: "Data Modeling",
|
||||
description: "Learn more about modeling",
|
||||
onClick: () => window.open(SplashScreen.dataModelingUrl),
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
iconSrc: undefined,
|
||||
title: "Cost & Throughput Calculation",
|
||||
description: "Learn more about cost calculation",
|
||||
onClick: () => window.open(SplashScreen.throughputEstimatorUrl),
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
iconSrc: undefined,
|
||||
title: "Configure automatic failover",
|
||||
description: "Learn more about Cosmos DB high-availability",
|
||||
onClick: () => window.open(SplashScreen.failoverUrl),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export function getQuotedCqlIdentifier(identifier: string): string {
|
||||
// Added return type optional undefined because passing undefined from test cases.
|
||||
export function getQuotedCqlIdentifier(identifier: string | undefined): string | undefined {
|
||||
let result = identifier;
|
||||
if (!identifier) {
|
||||
return result;
|
||||
|
||||
@@ -22,12 +22,15 @@ import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
||||
import TabsBase from "../TabsBase";
|
||||
import "./QueryTabComponent.less";
|
||||
|
||||
@@ -389,13 +392,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
};
|
||||
|
||||
public onSaveQueryClick = (): void => {
|
||||
this.props.collection && this.props.collection.container && this.props.collection.container.openSaveQueryPanel();
|
||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
||||
};
|
||||
|
||||
public onSavedQueriesClick = (): void => {
|
||||
this.props.collection &&
|
||||
this.props.collection.container &&
|
||||
this.props.collection.container.openBrowseQueriesPanel();
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
||||
};
|
||||
|
||||
public async onFetchNextPageClick(): Promise<void> {
|
||||
|
||||
@@ -137,13 +137,14 @@ export default class QueryTablesTab extends TabsBase {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Add Table Entity",
|
||||
"Add Table Row",
|
||||
<AddTableEntityPanel
|
||||
tableDataClient={this.tableDataClient}
|
||||
queryTablesTab={this}
|
||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
||||
cassandraApiClient={new CassandraAPIDataClient()}
|
||||
/>
|
||||
/>,
|
||||
"700px"
|
||||
);
|
||||
};
|
||||
|
||||
@@ -157,7 +158,8 @@ export default class QueryTablesTab extends TabsBase {
|
||||
queryTablesTab={this}
|
||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
||||
cassandraApiClient={new CassandraAPIDataClient()}
|
||||
/>
|
||||
/>,
|
||||
"700px"
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,18 +4,12 @@ import Collection from "./Collection";
|
||||
jest.mock("monaco-editor");
|
||||
|
||||
describe("Collection", () => {
|
||||
function generateCollection(
|
||||
container: Explorer,
|
||||
databaseId: string,
|
||||
data: DataModels.Collection,
|
||||
offer: DataModels.Offer
|
||||
): Collection {
|
||||
return new Collection(container, databaseId, data);
|
||||
}
|
||||
const generateCollection = (container: Explorer, databaseId: string, data: DataModels.Collection): Collection =>
|
||||
new Collection(container, databaseId, data);
|
||||
|
||||
function generateMockCollectionsDataModelWithPartitionKey(
|
||||
const generateMockCollectionsDataModelWithPartitionKey = (
|
||||
partitionKey: DataModels.PartitionKey
|
||||
): DataModels.Collection {
|
||||
): DataModels.Collection => {
|
||||
return {
|
||||
defaultTtl: 1,
|
||||
indexingPolicy: {} as DataModels.IndexingPolicy,
|
||||
@@ -26,13 +20,12 @@ describe("Collection", () => {
|
||||
_ts: 1,
|
||||
id: "",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection {
|
||||
const generateMockCollectionWithDataModel = (data: DataModels.Collection): Collection => {
|
||||
const mockContainer = {} as Explorer;
|
||||
|
||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
|
||||
}
|
||||
return generateCollection(mockContainer, "abc", data);
|
||||
};
|
||||
|
||||
describe("Partition key path parsing", () => {
|
||||
let collection: Collection;
|
||||
@@ -88,7 +81,7 @@ describe("Collection", () => {
|
||||
kind: "Hash",
|
||||
});
|
||||
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
||||
expect(collection.partitionKeyPropertyHeader).toBeNull;
|
||||
expect(collection.partitionKeyPropertyHeader).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -571,8 +571,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
};
|
||||
|
||||
public onSettingsClick = async (): Promise<void> => {
|
||||
await this.loadOffer();
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
await this.loadOffer();
|
||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
description: "Settings node",
|
||||
@@ -744,8 +744,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
StoredProcedure.create(source, event);
|
||||
}
|
||||
|
||||
public onNewUserDefinedFunctionClick(source: ViewModels.Collection, event: MouseEvent) {
|
||||
UserDefinedFunction.create(source, event);
|
||||
public onNewUserDefinedFunctionClick(source: ViewModels.Collection) {
|
||||
UserDefinedFunction.create(source);
|
||||
}
|
||||
|
||||
public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) {
|
||||
|
||||
@@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database {
|
||||
this.isOfferRead = false;
|
||||
}
|
||||
|
||||
public onSettingsClick = () => {
|
||||
public onSettingsClick = (): void => {
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
@@ -193,6 +193,8 @@ export default class Database implements ViewModels.Database {
|
||||
//merge collections
|
||||
this.addCollectionsToList(collectionVMs);
|
||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||
|
||||
useDatabases.getState().updateDatabase(this);
|
||||
}
|
||||
|
||||
public async openAddCollection(database: Database): Promise<void> {
|
||||
|
||||
@@ -1,45 +1,18 @@
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
public myNotebooksContentRoot: NotebookContentItem;
|
||||
export const ResourceTokenTree: React.FC = (): JSX.Element => {
|
||||
const collection = useDatabases((state) => state.resourceTokenCollection);
|
||||
|
||||
public constructor(private container: Explorer) {
|
||||
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;
|
||||
const buildCollectionNode = (): TreeNode => {
|
||||
if (!collection) {
|
||||
return {
|
||||
label: undefined,
|
||||
@@ -86,9 +59,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
|
||||
isExpanded: true,
|
||||
children: [collectionNode],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public triggerRender() {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
return <TreeComponent className="dataResourceTree" rootNode={buildCollectionNode()} />;
|
||||
};
|
||||
722
src/Explorer/Tree/ResourceTree.tsx
Normal file
722
src/Explorer/Tree/ResourceTree.tsx
Normal file
@@ -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,6 +16,7 @@ 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 { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
@@ -33,6 +34,7 @@ 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";
|
||||
@@ -624,7 +626,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
gitHubNotebooksTree.contextMenu = [
|
||||
{
|
||||
label: "Manage GitHub settings",
|
||||
onClick: () => this.container.openGitHubReposPanel("Manage GitHub settings"),
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Manage GitHub settings",
|
||||
<GitHubReposPanel
|
||||
explorer={this.container}
|
||||
gitHubClientProp={this.container.notebookManager.gitHubClient}
|
||||
junoClientProp={this.container.notebookManager.junoClient}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Disconnect from GitHub",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export default class UserDefinedFunction {
|
||||
this.body = ko.observable(data.body as string);
|
||||
}
|
||||
|
||||
public static create(source: ViewModels.Collection, event: MouseEvent) {
|
||||
public static create(source: ViewModels.Collection) {
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1;
|
||||
const userDefinedFunction = {
|
||||
id: "",
|
||||
@@ -104,7 +104,9 @@ export default class UserDefinedFunction {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
|
||||
this.collection.children.remove(this);
|
||||
},
|
||||
(reason) => {}
|
||||
() => {
|
||||
/**/
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -19,6 +19,8 @@ interface DatabasesState {
|
||||
loadDatabaseOffers: () => Promise<void>;
|
||||
isFirstResourceCreated: () => boolean;
|
||||
findSelectedDatabase: () => ViewModels.Database;
|
||||
validateDatabaseId: (id: string) => boolean;
|
||||
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||
@@ -129,4 +131,12 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||
|
||||
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);
|
||||
await database.loadCollections();
|
||||
return !database.collections().some((collection) => collection.id() === collectionId);
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user