Compare commits

...

8 Commits

Author SHA1 Message Date
Satyapriya Bai
9bb07f5098 updated snap. 2024-05-13 11:29:28 +05:30
Satyapriya Bai
cb15ef6f22 [accessibility-2262594]:[Keyboard Navigation - Azure Cosmos DB - Data Explorer]: Keyboard focus is not retaining back to 'more' button after closing 'Delete container' dialog. 2024-05-13 10:51:54 +05:30
Asier Isayas
14e5efcebf Point Mongo requests to old backend (#1838)
* point mongo requests to old backend

* point mongo requests to old backend

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-05-09 13:42:59 -04:00
Ashley Stanton-Nurse
5c3f18f5f8 add link to keyboard shortcuts doc to home tab (#1836) 2024-05-07 12:30:46 -07:00
jawelton74
6ebc48ad28 Remove some Notebooks code (#1832)
* Remove onNewNotebookClicked, openUploadFilePanel functions and
UploadFilePane.

* Remove resetNotebookWorkspace function.

* Remove Notebooks related resource tree node generation.

* Fix test snapshots.
2024-05-02 07:14:31 -07:00
jawelton74
298197b1b8 Revert "First set of changes for Notebooks removal. (#1816)" (#1830)
This reverts commit b023250e67.
2024-05-01 07:21:50 -07:00
Ashley Stanton-Nurse
81a5b7cb6d add shortcuts for the Items tab (#1827)
* add shortcuts for the Items tab

* Add shortcut to clear Items tab filter.
2024-04-30 10:03:27 -07:00
jawelton74
b023250e67 First set of changes for Notebooks removal. (#1816)
* First set of changes for Notebooks removal.

* Fix unit test snapshots.
2024-04-29 15:46:24 -07:00
19 changed files with 127 additions and 701 deletions

View File

@@ -102,13 +102,13 @@ let configContext: Readonly<ConfigContext> = {
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
NEW_MONGO_APIS: [ NEW_MONGO_APIS: [
"resourcelist", // "resourcelist",
"queryDocuments", // "queryDocuments",
"createDocument", // "createDocument",
"readDocument", // "readDocument",
"updateDocument", // "updateDocument",
"deleteDocument", // "deleteDocument",
"createCollectionWithProxy", // "createCollectionWithProxy",
"legacyMongoShell", "legacyMongoShell",
], ],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,

View File

@@ -132,13 +132,16 @@ export const createCollectionContextMenuButton = (
if (configContext.platform !== Platform.Fabric) { if (configContext.platform !== Platform.Fabric) {
items.push({ items.push({
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: () => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
useSelectedNode.getState().setSelectedNode(selectedCollection); useSelectedNode.getState().setSelectedNode(selectedCollection);
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
"Delete " + getCollectionName(), "Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />, <DeleteCollectionConfirmationPane
lastFocusedElement={lastFocusedElement}
refreshDatabases={() => container.refreshAllDatabases()}
/>,
); );
}, },
label: `Delete ${getCollectionName()}`, label: `Delete ${getCollectionName()}`,

View File

@@ -30,7 +30,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -108,7 +107,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -225,7 +223,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -272,7 +269,6 @@ exports[`SettingsComponent renders 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@@ -24,7 +24,7 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
export interface TreeNodeMenuItem { export interface TreeNodeMenuItem {
label: string; label: string;
onClick: () => void; onClick: (value?: React.RefObject<any>) => void;
iconSrc?: string; iconSrc?: string;
isDisabled?: boolean; isDisabled?: boolean;
styleClass?: string; styleClass?: string;
@@ -242,8 +242,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
}; };
return ( return (
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}> <div onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
<IconButton <IconButton
elementRef={this.contextMenuRef}
name="More" name="More"
title="More" title="More"
className="treeMenuEllipsis" className="treeMenuEllipsis"
@@ -283,7 +284,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
disabled: menuItem.isDisabled, disabled: menuItem.isDisabled,
className: menuItem.styleClass, className: menuItem.styleClass,
onClick: () => { onClick: () => {
menuItem.onClick(); menuItem.onClick(this.contextMenuRef);
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
label: menuItem.label, label: menuItem.label,
}); });

View File

@@ -174,6 +174,11 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
<CustomizedIconButton <CustomizedIconButton
ariaLabel="More options" ariaLabel="More options"
className="treeMenuEllipsis" className="treeMenuEllipsis"
elementRef={
Object {
"current": null,
}
}
menuIconProps={ menuIconProps={
Object { Object {
"iconName": "More", "iconName": "More",
@@ -399,6 +404,11 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
<CustomizedIconButton <CustomizedIconButton
ariaLabel="More options" ariaLabel="More options"
className="treeMenuEllipsis" className="treeMenuEllipsis"
elementRef={
Object {
"current": null,
}
}
menuIconProps={ menuIconProps={
Object { Object {
"iconName": "More", "iconName": "More",

View File

@@ -38,7 +38,6 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
@@ -56,7 +55,6 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
@@ -510,104 +508,6 @@ export default class Explorer {
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
} }
public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
"Explorer/resetNotebookWorkspace",
);
return;
}
const dialogContent = useNotebook.getState().isPhoenixNotebooks
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: dialogContent,
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
onSecondaryButtonClick: () => useDialog.getState().closeDialog(),
};
useDialog.getState().openDialog(resetConfirmationDialogProps);
}
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
if (!databaseAccount) {
return false;
}
try {
const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
);
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
return false;
}
}
private _resetNotebookWorkspace = async () => {
useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
let connectionStatus: ContainerConnectionInfo;
try {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
logConsoleError(error);
return;
}
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(true, connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (useNotebook.getState().isPhoenixNotebooks) {
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
throw error;
} finally {
clearInProgressMessage();
}
};
private getDeltaDatabases( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[], updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[], databases: ViewModels.Database[],
@@ -1010,92 +910,6 @@ export default class Explorer {
); );
} }
/**
* This creates a new notebook file, then opens the notebook
*/
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook,
});
this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent, isGithubTree)
.then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
},
startKey,
);
return this.openNotebook(newFile);
})
.then(() => this.resourceTree.triggerRender())
.catch((error) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
logConsoleError(errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey,
);
})
.finally(clearInProgressMessage);
}
// TODO: Delete this function when ResourceTreeAdapter is removed. // TODO: Delete this function when ResourceTreeAdapter is removed.
public async refreshContentItem(item: NotebookContentItem): Promise<void> { public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
@@ -1130,10 +944,6 @@ export default class Explorer {
let title: string; let title: string;
switch (kind) { switch (kind) {
case ViewModels.TerminalKind.Default:
title = "Terminal";
break;
case ViewModels.TerminalKind.Mongo: case ViewModels.TerminalKind.Mongo:
title = "Mongo Shell"; title = "Mongo Shell";
break; break;
@@ -1287,36 +1097,6 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />); .openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
} }
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (useNotebook.getState().isPhoenixNotebooks) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
useSidePanel
.getState()
.openSidePanel(
"Upload file to notebook server",
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
);
}
public getDownloadModalConent(fileName: string): JSX.Element { public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) { if (useNotebook.getState().isPhoenixNotebooks) {
return ( return (

View File

@@ -52,7 +52,9 @@ describe("Delete Collection Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => { describe("shouldRecordFeedback()", () => {
it("should return true if last collection and database does not have shared throughput else false", () => { it("should return true if last collection and database does not have shared throughput else false", () => {
const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />); const wrapper = shallow(
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
const database = { id: ko.observable("testDB") } as Database; const database = { id: ko.observable("testDB") } as Database;
@@ -109,7 +111,9 @@ describe("Delete Collection Confirmation Pane", () => {
}); });
it("should call delete collection", () => { it("should call delete collection", () => {
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />); const wrapper = mount(
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmCollectionId")).toBe(true); expect(wrapper.exists("#confirmCollectionId")).toBe(true);
@@ -126,7 +130,9 @@ describe("Delete Collection Confirmation Pane", () => {
}); });
it("should record feedback", async () => { it("should record feedback", async () => {
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />); const wrapper = mount(
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper.exists("#confirmCollectionId")).toBe(true); expect(wrapper.exists("#confirmCollectionId")).toBe(true);
wrapper wrapper
.find("#confirmCollectionId") .find("#confirmCollectionId")

View File

@@ -12,17 +12,19 @@ import { getCollectionName } from "Utils/APITypeUtils";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface DeleteCollectionConfirmationPaneProps { export interface DeleteCollectionConfirmationPaneProps {
refreshDatabases: () => Promise<void>; refreshDatabases: () => Promise<void>;
lastFocusedElement: React.MutableRefObject<HTMLElement>;
} }
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
refreshDatabases, refreshDatabases,
lastFocusedElement,
}: DeleteCollectionConfirmationPaneProps) => { }: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>(""); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
@@ -35,6 +37,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName; const paneTitle = "Delete " + collectionName;
const lastItemElement = lastFocusedElement?.current;
const onSubmit = async (): Promise<void> => { const onSubmit = async (): Promise<void> => {
const collection = useSelectedNode.getState().findSelectedCollection(); const collection = useSelectedNode.getState().findSelectedCollection();
@@ -111,6 +114,13 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
}; };
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`; const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`; const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
useEffect(() => {
return () => {
if (lastItemElement) {
lastItemElement.focus();
}
};
}, [lastItemElement]);
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">

View File

@@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
Object { Object {
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@@ -1,91 +0,0 @@
import { Upload } from "Common/Upload/Upload";
import { useSidePanel } from "hooks/useSidePanel";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface UploadFilePanelProps {
uploadFile: (name: string, content: string) => Promise<NotebookContentItem>;
}
export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({ uploadFile }: UploadFilePanelProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const extensions: string = undefined; //ex. ".ipynb"
const errorMessage = "Could not upload file";
const inProgressMessage = "Uploading file to notebook server";
const successMessage = "Successfully uploaded file to notebook server";
const [files, setFiles] = useState<FileList>();
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = () => {
setFormErrors("");
if (!files || files.length === 0) {
setFormErrors("No file specified. Please input a file.");
logConsoleError(`${errorMessage} -- No file specified. Please input a file.`);
return;
}
const file: File = files.item(0);
const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`);
setIsExecuting(true);
onSubmit(files.item(0))
.then(
() => {
logConsoleInfo(`${successMessage} ${file.name}`);
closeSidePanel();
},
(error: string) => {
setFormErrors(errorMessage);
logConsoleError(`${errorMessage} ${file.name}: ${error}`);
},
)
.finally(() => {
setIsExecuting(false);
clearMessage();
});
};
const updateSelectedFiles = (event: ChangeEvent<HTMLInputElement>): void => {
setFiles(event.target.files);
};
const onSubmit = async (file: File): Promise<NotebookContentItem> => {
const readFileAsText = (inputFile: File): Promise<string> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => {
reader.abort();
reject(`Problem parsing file: ${inputFile}`);
};
reader.onload = () => {
resolve(reader.result as string);
};
reader.readAsText(inputFile);
});
};
const fileContent = await readFileAsText(file);
return uploadFile(file.name, fileContent);
};
const props: RightPaneFormProps = {
formError: formErrors,
isExecuting: isExecuting,
submitButtonText: "Upload",
onSubmit: submit,
};
return (
<RightPaneForm {...props}>
<div className="paneMainContent">
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
</div>
</RightPaneForm>
);
};

View File

@@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@@ -25,7 +25,6 @@ import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg"; import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg"; import ContainersIcon from "../../../images/Containers.svg";
import LinkIcon from "../../../images/Link_blue.svg"; import LinkIcon from "../../../images/Link_blue.svg";
import NotebookColorIcon from "../../../images/Notebooks.svg";
import PowerShellIcon from "../../../images/PowerShell.svg"; import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
@@ -410,14 +409,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}, },
}; };
heroes.push(launchQuickstartBtn); heroes.push(launchQuickstartBtn);
} else if (useNotebook.getState().isPhoenixNotebooks) {
const newNotebookBtn = {
iconSrc: NotebookColorIcon,
title: "New notebook",
description: "Visualize your data stored in Azure Cosmos DB",
onClick: () => this.container.onNewNotebookClicked(),
};
heroes.push(newNotebookBtn);
} }
heroes.push(this.getShellCard()); heroes.push(this.getShellCard());
@@ -689,11 +680,20 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Learn the Fundamentals", title: "Learn the Fundamentals",
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.", description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
}; };
let items: item[];
const commonItems: item[] = [
{
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
title: "Data Explorer keyboard shortcuts",
description: "Learn keyboard shortcuts to navigate Data Explorer.",
},
];
let apiItems: item[];
switch (userContext.apiType) { switch (userContext.apiType) {
case "SQL": case "SQL":
case "Postgres": case "Postgres":
items = [ apiItems = [
{ {
link: "https://aka.ms/msl-sdk-connect", link: "https://aka.ms/msl-sdk-connect",
title: "Get Started using an SDK", title: "Get Started using an SDK",
@@ -708,7 +708,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Mongo": case "Mongo":
items = [ apiItems = [
{ {
link: "https://aka.ms/mongonodejs", link: "https://aka.ms/mongonodejs",
title: "Build an app with Node.js", title: "Build an app with Node.js",
@@ -723,7 +723,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Cassandra": case "Cassandra":
items = [ apiItems = [
{ {
link: "https://aka.ms/cassandracontainer", link: "https://aka.ms/cassandracontainer",
title: "Create a Container", title: "Create a Container",
@@ -738,7 +738,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Gremlin": case "Gremlin":
items = [ apiItems = [
{ {
link: "https://aka.ms/graphquickstart", link: "https://aka.ms/graphquickstart",
title: "Get Started ", title: "Get Started ",
@@ -753,7 +753,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
]; ];
break; break;
case "Tables": case "Tables":
items = [ apiItems = [
{ {
link: "https://aka.ms/tabledotnet", link: "https://aka.ms/tabledotnet",
title: "Build a .NET App", title: "Build a .NET App",
@@ -770,6 +770,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
default: default:
break; break;
} }
const items = [...commonItems, ...apiItems];
return ( return (
<Stack> <Stack>
{items.map((item, i) => ( {items.map((item, i) => (

View File

@@ -80,7 +80,8 @@
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.' placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
}, },
css: { placeholderVisible: filterContent().length === 0 }, css: { placeholderVisible: filterContent().length === 0 },
textInput: filterContent" textInput: filterContent,
event: { keydown: onFilterKeyDown }"
/> />
<datalist <datalist

View File

@@ -1,7 +1,7 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, KeyboardHandlerSetter, useKeyboardActionGroup } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as ko from "knockout"; import * as ko from "knockout";
@@ -86,9 +86,11 @@ export default class DocumentsTab extends TabsBase {
private _isQueryCopilotSampleContainer: boolean; private _isQueryCopilotSampleContainer: boolean;
private queryAbortController: AbortController; private queryAbortController: AbortController;
private cancelQueryTimeoutID: NodeJS.Timeout; private cancelQueryTimeoutID: NodeJS.Timeout;
private setKeyboardActions: KeyboardHandlerSetter;
constructor(options: ViewModels.DocumentsTabOptions) { constructor(options: ViewModels.DocumentsTabOptions) {
super(options); super(options);
this.setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB; this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
@@ -665,9 +667,38 @@ export default class DocumentsTab extends TabsBase {
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
} }
public onFilterKeyDown(model: unknown, e: KeyboardEvent): boolean {
if (e.key === "Enter") {
this.refreshDocumentsGrid(true);
// Suppress the default behavior of the key
return false;
} else if (e.key === "Escape") {
this.onHideFilterClick();
// Suppress the default behavior of the key
return false;
} else {
// Allow the default behavior of the key
return true;
}
}
public async onActivate(): Promise<void> { public async onActivate(): Promise<void> {
super.onActivate(); super.onActivate();
this.setKeyboardActions({
[KeyboardAction.SEARCH]: () => {
this.onShowFilterClick();
return true;
},
[KeyboardAction.CLEAR_SEARCH]: () => {
this.filterContent("");
this.refreshDocumentsGrid(true);
return true;
},
});
if (!this._documentsIterator) { if (!this._documentsIterator) {
try { try {
await this.autoPopulateContent(); await this.autoPopulateContent();

View File

@@ -1,3 +1,4 @@
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as ThemeUtility from "../../Common/ThemeUtility"; import * as ThemeUtility from "../../Common/ThemeUtility";
@@ -107,6 +108,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
} }
public onActivate(): void { public onActivate(): void {
clearKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
this.updateSelectedNode(); this.updateSelectedNode();
this.collection?.selectedSubnodeKind(this.tabKind); this.collection?.selectedSubnodeKind(this.tabKind);
this.database?.selectedSubnodeKind(this.tabKind); this.database?.selectedSubnodeKind(this.tabKind);

View File

@@ -1,11 +1,9 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as React from "react"; import * as React from "react";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
@@ -14,17 +12,14 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
@@ -36,7 +31,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil"; import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -75,152 +69,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
const pseudoDirPath = "PsuedoDir"; 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 (!useNotebook.getState().isPhoenixNotebooks) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (
myNotebooksContentRoot &&
useNotebook.getState().isPhoenixNotebooks &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
) {
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(true));
}
}
return notebooksTree;
};
const buildNotebooksTemporarilyDownTree = (): TreeNode => {
return {
label: Notebook.temporarilyDownMsg,
className: "clickDisabled",
};
};
const buildGalleryNotebooksTree = (): TreeNode => {
return {
label: "Gallery",
iconSrc: GalleryIcon,
className: "notebookHeader galleryHeader",
onClick: () => container.openGallery(),
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
};
};
const buildMyNotebooksTree = (): TreeNode => {
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item);
},
);
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 = (isConnected: boolean): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item);
},
true,
);
const manageGitContextMenu: TreeNodeMenuItem[] = [
{
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.contextMenu = manageGitContextMenu;
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;
return gitHubNotebooksTree;
};
const buildChildNodes = ( const buildChildNodes = (
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void,
@@ -373,11 +221,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => container.onCreateDirectory(item, isGithubTree), onClick: () => container.onCreateDirectory(item, isGithubTree),
}, },
{
label: "Upload File",
iconSrc: NewNotebookIcon,
onClick: () => container.openUploadFilePanel(item),
},
]; ];
//disallow renaming of temporary notebook workspace //disallow renaming of temporary notebook workspace
@@ -782,8 +625,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} /> <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
{/* {buildGalleryCallout()} */}
</> </>
)} )}
{!isNotebookEnabled && isSampleDataEnabled && ( {!isNotebookEnabled && isSampleDataEnabled && (
@@ -796,8 +637,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} /> <SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent> </AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
{/* {buildGalleryCallout()} */}
</> </>
)} )}
{isNotebookEnabled && isSampleDataEnabled && ( {isNotebookEnabled && isSampleDataEnabled && (
@@ -809,12 +648,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}> <AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} /> <SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
</AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
{/* {buildGalleryCallout()} */}
</> </>
)} )}
</> </>

View File

@@ -1,9 +1,7 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import * as ko from "knockout"; import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
@@ -13,21 +11,17 @@ import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { Areas } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog"; import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -36,7 +30,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil"; import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -102,27 +95,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
const dataRootNode = this.buildDataTree(); const dataRootNode = this.buildDataTree();
const notebooksRootNode = this.buildNotebooksTrees();
if (useNotebook.getState().isNotebookEnabled) {
return (
<>
<AccordionComponent>
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
</AccordionItemComponent>
</AccordionComponent>
{/* {this.galleryContentRoot && this.buildGalleryCallout()} */}
</>
);
} else {
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />; return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
} }
}
public async initialize(): Promise<void[]> { public async initialize(): Promise<void[]> {
const refreshTasks: Promise<void>[] = []; const refreshTasks: Promise<void>[] = [];
@@ -504,156 +478,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
return traverse(schema); return traverse(schema);
} }
private buildNotebooksTrees(): TreeNode {
let notebooksTree: TreeNode = {
label: undefined,
isExpanded: true,
children: [],
};
if (this.galleryContentRoot) {
notebooksTree.children.push(this.buildGalleryNotebooksTree());
}
if (this.myNotebooksContentRoot) {
notebooksTree.children.push(this.buildMyNotebooksTree());
}
if (this.gitHubNotebooksContentRoot) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(this.buildGitHubNotebooksTree());
}
return notebooksTree;
}
private 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);
this.triggerRender();
},
setInitialFocus: true,
};
const openGalleryProps: ILinkProps = {
onClick: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
this.container.openGallery();
this.triggerRender();
},
};
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>
);
}
private buildGalleryNotebooksTree(): TreeNode {
return {
label: "Gallery",
iconSrc: GalleryIcon,
className: "notebookHeader galleryHeader",
onClick: () => this.container.openGallery(),
isSelected: () => {
const activeTab = useTabs.getState().activeTab;
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
},
};
}
private buildMyNotebooksTree(): TreeNode {
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.myNotebooksContentRoot,
(item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
},
true,
true,
);
myNotebooksTree.isExpanded = true;
myNotebooksTree.isAlphaSorted = true;
// Remove "Delete" menu item from context menu
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
return myNotebooksTree;
}
private buildGitHubNotebooksTree(): TreeNode {
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
},
true,
true,
);
gitHubNotebooksTree.contextMenu = [
{
label: "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",
onClick: () => {
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
this.container.notebookManager?.gitHubOAuthService.logout();
},
},
];
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;
return gitHubNotebooksTree;
}
private buildChildNodes( private buildChildNodes(
item: NotebookContentItem, item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void, onFileClick: (item: NotebookContentItem) => void,
@@ -800,11 +624,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item), onClick: () => this.container.onCreateDirectory(item),
}, },
{
label: "Upload File",
iconSrc: NewNotebookIcon,
onClick: () => this.container.openUploadFilePanel(item),
},
]; ];
//disallow renaming of temporary notebook workspace //disallow renaming of temporary notebook workspace

View File

@@ -17,8 +17,17 @@ export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHa
* Each group can be updated separately, but, when updated, must be completely replaced. * Each group can be updated separately, but, when updated, must be completely replaced.
*/ */
export enum KeyboardActionGroup { export enum KeyboardActionGroup {
/** Keyboard actions related to tab navigation. */
TABS = "TABS", TABS = "TABS",
/** Keyboard actions managed by the global command bar. */
COMMAND_BAR = "COMMAND_BAR", COMMAND_BAR = "COMMAND_BAR",
/**
* Keyboard actions specific to the active tab.
* This group is automatically cleared when the active tab changes.
*/
ACTIVE_TAB = "ACTIVE_TAB",
} }
/** /**
@@ -43,6 +52,8 @@ export enum KeyboardAction {
SELECT_LEFT_TAB = "SELECT_LEFT_TAB", SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB", SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
CLOSE_TAB = "CLOSE_TAB", CLOSE_TAB = "CLOSE_TAB",
SEARCH = "SEARCH",
CLEAR_SEARCH = "CLEAR_SEARCH",
} }
/** /**
@@ -72,6 +83,8 @@ const bindings: Record<KeyboardAction, string[]> = {
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"], [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"], [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
[KeyboardAction.SEARCH]: ["$mod+Shift+F"],
[KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"],
}; };
interface KeyboardShortcutState { interface KeyboardShortcutState {
@@ -91,14 +104,25 @@ interface KeyboardShortcutState {
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void; setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
} }
export type KeyboardHandlerSetter = (handlers: KeyboardHandlerMap) => void;
/** /**
* Defines the calling component as the manager of the keyboard actions for the given group. * Defines the calling component as the manager of the keyboard actions for the given group.
* @param group The group of keyboard actions to manage. * @param group The group of keyboard actions to manage.
* @returns A function that can be used to set the keyboard action handlers for the given group. * @returns A function that can be used to set the keyboard action handlers for the given group.
*/ */
export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => export const useKeyboardActionGroup: (group: KeyboardActionGroup) => KeyboardHandlerSetter =
(group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
useKeyboardActionHandlers.getState().setHandlers(group, handlers); useKeyboardActionHandlers.getState().setHandlers(group, handlers);
/**
* Clears the keyboard action handlers for the given group.
* @param group The group of keyboard actions to clear.
*/
export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => {
useKeyboardActionHandlers.getState().setHandlers(group, {});
};
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({ const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
allHandlers: {}, allHandlers: {},
groups: {}, groups: {},