From a66fc06dad737468facb24d8b7a9beb3dd584555 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Wed, 28 Jul 2021 21:29:45 -0700 Subject: [PATCH 01/60] Turn off react resource tree (#963) --- src/Common/ResourceTreeContainer.tsx | 6 +++--- src/Platform/Hosted/extractFeatures.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index fe04f9e04..ca41610da 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -54,10 +54,10 @@ export const ResourceTreeContainer: FunctionComponent {userContext.authType === AuthType.ResourceToken ? ( - ) : userContext.features.enableKOResourceTree ? ( -
- ) : ( + ) : userContext.features.enableReactResourceTree ? ( + ) : ( +
)}
{/* Collections Window - End */} diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 673d0c255..f1f0115a5 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -15,7 +15,7 @@ export type Features = { readonly enableTtl: boolean; readonly executeSproc: boolean; readonly enableAadDataPlane: boolean; - readonly enableKOResourceTree: boolean; + readonly enableReactResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -57,7 +57,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), - enableKOResourceTree: "true" === get("enablekoresourcetree"), + enableReactResourceTree: "true" === get("enablereactresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), junoEndpoint: get("junoendpoint"), From 7e0c4b72909110c9862b6ee76fec526f08247d83 Mon Sep 17 00:00:00 2001 From: t-tarabhatia <84812001+t-tarabhatia@users.noreply.github.com> Date: Thu, 29 Jul 2021 06:48:03 -0700 Subject: [PATCH 02/60] Add changes to Partition Key A/B Test (#954) --- src/Common/Constants.ts | 1 + src/Explorer/Panes/AddCollectionPanel.tsx | 19 ++++++++++++++----- src/Platform/Hosted/extractFeatures.ts | 2 ++ src/hooks/useKnockoutExplorer.ts | 6 ++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index fc97d559d..cbdd2da8e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -95,6 +95,7 @@ export class Flights { public static readonly MongoIndexing = "mongoindexing"; public static readonly AutoscaleTest = "autoscaletest"; public static readonly PartitionKeyTest = "partitionkeytest"; + public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; } export class AfecFeatures { diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index f0ce4f4d1..39d7ad2c8 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -113,11 +113,7 @@ export class AddCollectionPanel extends React.Component Date: Fri, 30 Jul 2021 10:27:27 -0700 Subject: [PATCH 03/60] Replace window.confirm and window.alert with modal dialog (#965) --- src/Explorer/Controls/Dialog.tsx | 55 +++++++++++++++- .../QueriesGridComponent.tsx | 66 +++++++++++-------- .../DataSamples/DataSamplesUtil.test.ts | 1 - src/Explorer/DataSamples/DataSamplesUtil.ts | 5 +- src/Explorer/Explorer.tsx | 58 +++------------- .../Notebook/NotebookComponent/epics.ts | 35 +++++----- src/Explorer/Notebook/NotebookManager.tsx | 41 ++++++------ .../Tables/DataTable/TableCommands.ts | 34 ++++++---- src/Explorer/Tabs/ConflictsTab.ts | 29 +++++--- src/Explorer/Tabs/DocumentsTab.ts | 48 +++++++++----- src/Explorer/Tabs/MongoDocumentsTab.ts | 5 +- src/Explorer/Tabs/NotebookV2Tab.ts | 19 +++--- src/Explorer/Tree/ConflictId.ts | 28 ++++---- src/Explorer/Tree/DocumentId.ts | 15 ++++- src/Explorer/Tree/ResourceTree.tsx | 37 ++++++----- src/Explorer/Tree/ResourceTreeAdapter.tsx | 37 ++++++----- src/Explorer/Tree/StoredProcedure.ts | 22 ++++--- src/Explorer/Tree/Trigger.ts | 22 ++++--- src/Explorer/Tree/UserDefinedFunction.ts | 26 +++++--- src/Utils/GalleryUtils.test.ts | 17 ++--- src/Utils/GalleryUtils.ts | 6 +- tsconfig.strict.json | 1 - 22 files changed, 353 insertions(+), 254 deletions(-) diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index ad9805348..d5e308d69 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -23,13 +23,66 @@ export interface DialogState { dialogProps?: DialogProps; openDialog: (props: DialogProps) => void; closeDialog: () => void; + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ) => void; + showOkModalDialog: (title: string, subText: string) => void; } -export const useDialog: UseStore = create((set) => ({ +export const useDialog: UseStore = create((set, get) => ({ visible: false, openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), closeDialog: () => set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: okLabel, + secondaryButtonText: cancelLabel, + onPrimaryButtonClick: () => { + get().closeDialog(); + onOk && onOk(); + }, + onSecondaryButtonClick: () => { + get().closeDialog(); + onCancel && onCancel(); + }, + choiceGroupProps, + textFieldProps, + primaryButtonDisabled, + }), + showOkModalDialog: (title: string, subText: string): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: () => { + get().closeDialog(); + }, + onSecondaryButtonClick: undefined, + }), })); export interface TextFieldProps extends ITextFieldProps { diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx index 3c3ef24b7..236dea5c3 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx @@ -29,6 +29,7 @@ import { QueriesClient } from "../../../Common/QueriesClient"; import * as DataModels from "../../../Contracts/DataModels"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Dialog"; const title = "Open Saved Queries"; @@ -222,35 +223,42 @@ export class QueriesGridComponent extends React.Component { - if (window.confirm("Are you sure you want to delete this query?")) { - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - }); - try { - await this.props.queriesClient.deleteQuery(query); - TelemetryProcessor.traceSuccess( - Action.DeleteSavedQuery, - { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - }, - startKey - ); - } catch (error) { - TelemetryProcessor.traceFailure( - Action.DeleteSavedQuery, - { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - } - await this.fetchSavedQueries(); // get latest state - } + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete this query?", + "Delete", + async () => { + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + }); + try { + await this.props.queriesClient.deleteQuery(query); + TelemetryProcessor.traceSuccess( + Action.DeleteSavedQuery, + { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + }, + startKey + ); + } catch (error) { + TelemetryProcessor.traceFailure( + Action.DeleteSavedQuery, + { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + await this.fetchSavedQueries(); // get latest state + }, + "Cancel", + undefined + ); }, }, ], diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index f8ff6f8e5..c8ae2e66a 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -17,7 +17,6 @@ describe("DataSampleUtils", () => { collections: ko.observableArray([collection]), } as Database; const explorer = {} as Explorer; - explorer.showOkModalDialog = () => {}; useDatabases.getState().addDatabases([database]); const dataSamplesUtil = new DataSamplesUtil(explorer); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index 4007608c0..d28ef0426 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -1,6 +1,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; @@ -20,7 +21,7 @@ export class DataSamplesUtil { const containerName = generator.getCollectionId(); if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; - this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); + useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleError(msg); return; } @@ -29,7 +30,7 @@ export class DataSamplesUtil { .createSampleContainerAsync() .catch((error) => logConsoleError(`Error creating sample container: ${error}`)); const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; - this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); + useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleInfo(msg); } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 79f6fec11..a4cd403f0 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,4 +1,3 @@ -import { IChoiceGroupProps } from "@fluentui/react"; import * as ko from "knockout"; import React from "react"; import _ from "underscore"; @@ -35,7 +34,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import "./ComponentRegisterer"; -import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog"; +import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import * as FileSystemUtil from "./Notebook/FileSystemUtil"; @@ -548,7 +547,7 @@ export default class Explorer { const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); promise .then(() => this.resourceTree.triggerRender()) - .catch((reason) => this.showOkModalDialog("Unable to upload file", reason)); + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", reason)); return promise; } @@ -614,51 +613,6 @@ export default class Explorer { this.notebookManager?.openCopyNotebookPane(name, content); } - public showOkModalDialog(title: string, msg: string): void { - useDialog.getState().openDialog({ - isModal: true, - title, - subText: msg, - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: () => { - useDialog.getState().closeDialog(); - }, - onSecondaryButtonClick: undefined, - }); - } - - public showOkCancelModalDialog( - title: string, - msg: string, - okLabel: string, - onOk: () => void, - cancelLabel: string, - onCancel: () => void, - choiceGroupProps?: IChoiceGroupProps, - textFieldProps?: TextFieldProps, - isPrimaryButtonDisabled?: boolean - ): void { - useDialog.getState().openDialog({ - isModal: true, - title, - subText: msg, - primaryButtonText: okLabel, - secondaryButtonText: cancelLabel, - onPrimaryButtonClick: () => { - useDialog.getState().closeDialog(); - onOk && onOk(); - }, - onSecondaryButtonClick: () => { - useDialog.getState().closeDialog(); - onCancel && onCancel(); - }, - choiceGroupProps, - textFieldProps, - primaryButtonDisabled: isPrimaryButtonDisabled, - }); - } - /** * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. @@ -732,7 +686,9 @@ export default class Explorer { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); }); if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); + useDialog + .getState() + .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); } else { useSidePanel.getState().openSidePanel( "Rename Notebook", @@ -862,7 +818,9 @@ export default class Explorer { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); }); if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); + useDialog + .getState() + .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); return Promise.reject(); } diff --git a/src/Explorer/Notebook/NotebookComponent/epics.ts b/src/Explorer/Notebook/NotebookComponent/epics.ts index 718028dab..9e6616a06 100644 --- a/src/Explorer/Notebook/NotebookComponent/epics.ts +++ b/src/Explorer/Notebook/NotebookComponent/epics.ts @@ -38,6 +38,7 @@ import { useTabs } from "../../../hooks/useTabs"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; +import { useDialog } from "../../Controls/Dialog"; import * as FileSystemUtil from "../FileSystemUtil"; import * as cdbActions from "../NotebookComponent/actions"; import { NotebookUtil } from "../NotebookUtil"; @@ -686,10 +687,8 @@ const handleKernelConnectionLostEpic = ( logConsoleError(msg); logFailureToTelemetry(state, "Kernel restart error", msg); - const explorer = window.dataExplorer; - if (explorer) { - explorer.showOkModalDialog("kernel restarts", msg); - } + useDialog.getState().showOkModalDialog("kernel restarts", msg); + return of(EMPTY); } @@ -773,8 +772,7 @@ const closeUnsupportedMimetypesEpic = ( ofType(actions.FETCH_CONTENT_FULFILLED), mergeMap((action) => { const mimetype = action.payload.model.mimetype; - const explorer = window.dataExplorer; - if (explorer && !TextFile.handles(mimetype)) { + if (!TextFile.handles(mimetype)) { const filepath = action.payload.filepath; // Close tab and show error message useTabs @@ -783,7 +781,7 @@ const closeUnsupportedMimetypesEpic = ( (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) ); const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; - explorer.showOkModalDialog("File cannot be rendered", msg); + useDialog.getState().showOkModalDialog("File cannot be rendered", msg); logConsoleError(msg); } return EMPTY; @@ -803,19 +801,16 @@ const closeContentFailedToFetchEpic = ( return action$.pipe( ofType(actions.FETCH_CONTENT_FAILED), mergeMap((action) => { - const explorer = window.dataExplorer; - if (explorer) { - const filepath = action.payload.filepath; - // Close tab and show error message - useTabs - .getState() - .closeTabsByComparator( - (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) - ); - const msg = `Failed to load file: ${filepath}.`; - explorer.showOkModalDialog("Failure to load", msg); - logConsoleError(msg); - } + const filepath = action.payload.filepath; + // Close tab and show error message + useTabs + .getState() + .closeTabsByComparator( + (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) + ); + const msg = `Failed to load file: ${filepath}.`; + useDialog.getState().showOkModalDialog("Failure to load", msg); + logConsoleError(msg); return EMPTY; }) ); diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 4d14abf6a..ed66caab1 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -18,6 +18,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; @@ -172,31 +173,33 @@ export default class NotebookManager { if (error.status === HttpStatusCodes.Unauthorized) { this.gitHubOAuthService.resetToken(); - this.params.container.showOkCancelModalDialog( - undefined, - "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", - "Connect to GitHub", - () => - useSidePanel - .getState() - .openSidePanel( - "Connect to GitHub", - - ), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + undefined, + "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", + "Connect to GitHub", + () => + useSidePanel + .getState() + .openSidePanel( + "Connect to GitHub", + + ), + "Cancel", + undefined + ); } }; private promptForCommitMsg = (title: string, primaryButtonLabel: string) => { return new Promise((resolve, reject) => { let commitMsg = "Committed from Azure Cosmos DB Notebooks"; - this.params.container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( title || "Commit", undefined, primaryButtonLabel || "Commit", diff --git a/src/Explorer/Tables/DataTable/TableCommands.ts b/src/Explorer/Tables/DataTable/TableCommands.ts index 9da9746ee..c5a80f65e 100644 --- a/src/Explorer/Tables/DataTable/TableCommands.ts +++ b/src/Explorer/Tables/DataTable/TableCommands.ts @@ -1,5 +1,6 @@ import Q from "q"; import { userContext } from "../../../UserContext"; +import { useDialog } from "../../Controls/Dialog"; import Explorer from "../../Explorer"; import * as Entities from "../Entities"; import * as DataTableUtilities from "./DataTableUtilities"; @@ -69,19 +70,28 @@ export default class TableCommands { return null; // Error } var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); - let deleteMessage: string = "Are you sure you want to delete the selected entities?"; - if (userContext.apiType === "Cassandra") { - deleteMessage = "Are you sure you want to delete the selected rows?"; - } - if (window.confirm(deleteMessage)) { - viewModel.queryTablesTab.container.tableDataClient - .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) - .then((results: any) => { - return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { - viewModel.redrawTableThrottled(); + const deleteMessage: string = + userContext.apiType === "Cassandra" + ? "Are you sure you want to delete the selected rows?" + : "Are you sure you want to delete the selected entities?"; + + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + deleteMessage, + "Delete", + () => { + viewModel.queryTablesTab.container.tableDataClient + .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) + .then((results: any) => { + return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { + viewModel.redrawTableThrottled(); + }); }); - }); - } + }, + "Cancel", + undefined + ); + return null; } diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index 6d95b01eb..fb51394e2 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -21,6 +21,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import ConflictId from "../Tree/ConflictId"; @@ -228,7 +229,7 @@ export default class ConflictsTab extends TabsBase { this._documentsIterator = this.createIterator(); await this.loadNextPage(); } catch (error) { - window.alert(getErrorMessage(error)); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); } } @@ -252,10 +253,23 @@ export default class ConflictsTab extends TabsBase { } public onAcceptChangesClick = async (): Promise => { - if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return; + if (this.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Changes will be lost. Do you want to continue?", + "OK", + async () => await this.resolveConflict(), + "Cancel", + undefined + ); + } else { + await this.resolveConflict(); } + }; + private resolveConflict = async (): Promise => { this.isExecutionError(false); this.isExecuting(true); @@ -318,7 +332,7 @@ export default class ConflictsTab extends TabsBase { } catch (error) { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage); TelemetryProcessor.traceFailure( Action.ResolveConflict, { @@ -372,7 +386,7 @@ export default class ConflictsTab extends TabsBase { } catch (error) { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage); TelemetryProcessor.traceFailure( Action.DeleteConflict, { @@ -662,11 +676,6 @@ export default class ConflictsTab extends TabsBase { return jsonObject; } - private _isIgnoreDirtyEditor = (): boolean => { - var msg: string = "Changes will be lost. Do you want to continue?"; - return window.confirm(msg); - }; - private _getPartitionKeyPropertyHeader(): string { return ( (this.partitionKey && diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 2968a3511..504797a68 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -25,6 +25,7 @@ import { userContext } from "../../UserContext"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import * as QueryUtils from "../../Utils/QueryUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import DocumentId from "../Tree/DocumentId"; @@ -378,7 +379,7 @@ export default class DocumentsTab extends TabsBase { this.isFilterExpanded(false); document.getElementById("errorStatusIcon")?.focus(); } catch (error) { - window.alert(getErrorMessage(error)); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); } } @@ -401,18 +402,29 @@ export default class DocumentsTab extends TabsBase { return Q(); } - public onNewDocumentClick = (): Q.Promise => { - if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return Q(); + public onNewDocumentClick = (): void => { + if (this.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Changes will be lost. Do you want to continue?", + "OK", + () => this.initializeNewDocument(), + "Cancel", + undefined + ); + } else { + this.initializeNewDocument(); } - this.selectedDocumentId(null); + }; + private initializeNewDocument = (): void => { + this.selectedDocumentId(null); const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); this.initialDocumentContent(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - - return Q(); }; public onSaveNewDocumentClick = (): Promise => { @@ -453,7 +465,7 @@ export default class DocumentsTab extends TabsBase { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { @@ -516,7 +528,7 @@ export default class DocumentsTab extends TabsBase { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { @@ -546,9 +558,16 @@ export default class DocumentsTab extends TabsBase { ? "Are you sure you want to delete the selected item ?" : "Are you sure you want to delete the selected document ?"; - if (window.confirm(msg)) { - await this._deleteDocument(selectedDocumentId); - } + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + msg, + "Delete", + async () => await this._deleteDocument(selectedDocumentId), + "Cancel", + undefined + ); }; public onValidDocumentEdit(): Q.Promise { @@ -617,11 +636,6 @@ export default class DocumentsTab extends TabsBase { } } - private _isIgnoreDirtyEditor = (): boolean => { - var msg: string = "Changes will be lost. Do you want to continue?"; - return window.confirm(msg); - }; - protected __deleteDocument(documentId: DocumentId): Promise { return deleteDocument(this.collection, documentId); } diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index 3b3866b9e..fff7cb042 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -16,6 +16,7 @@ import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import DocumentId from "../Tree/DocumentId"; import ObjectId from "../Tree/ObjectId"; import DocumentsTab from "./DocumentsTab"; @@ -111,7 +112,7 @@ export default class MongoDocumentsTab extends DocumentsTab { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { @@ -169,7 +170,7 @@ export default class MongoDocumentsTab extends DocumentsTab { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 012406760..01aab3c6a 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -17,6 +17,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; import * as CdbActions from "../Notebook/NotebookComponent/actions"; @@ -59,14 +60,16 @@ export default class NotebookTabV2 extends NotebookTabBase { }; if (this.notebookComponentAdapter.isContentDirty()) { - this.container.showOkCancelModalDialog( - "Close without saving?", - `File has unsaved changes, close without saving?`, - "Close", - cleanup, - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Close without saving?", + `File has unsaved changes, close without saving?`, + "Close", + cleanup, + "Cancel", + undefined + ); return Q.resolve(null); } else { cleanup(); diff --git a/src/Explorer/Tree/ConflictId.ts b/src/Explorer/Tree/ConflictId.ts index c9cf1a98a..89185b80a 100644 --- a/src/Explorer/Tree/ConflictId.ts +++ b/src/Explorer/Tree/ConflictId.ts @@ -1,12 +1,11 @@ -import Q from "q"; +import { extractPartitionKey } from "@azure/cosmos"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; -import DocumentId from "./DocumentId"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { extractPartitionKey } from "@azure/cosmos"; -import ConflictsTab from "../Tabs/ConflictsTab"; import { readDocument } from "../../Common/dataAccess/readDocument"; +import * as DataModels from "../../Contracts/DataModels"; +import { useDialog } from "../Controls/Dialog"; +import ConflictsTab from "../Tabs/ConflictsTab"; +import DocumentId from "./DocumentId"; export default class ConflictId { public container: ConflictsTab; @@ -50,13 +49,20 @@ export default class ConflictId { } public click() { - if ( - !this.container.isEditorDirty() || - window.confirm("Your unsaved changes will be lost. Do you want to continue?") - ) { + if (this.container.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Your unsaved changes will be lost. Do you want to continue?", + "OK", + () => this.loadConflict(), + "Cancel", + undefined + ); + } else { this.loadConflict(); } - return; } public async loadConflict(): Promise { diff --git a/src/Explorer/Tree/DocumentId.ts b/src/Explorer/Tree/DocumentId.ts index a30c0b534..af2d0250c 100644 --- a/src/Explorer/Tree/DocumentId.ts +++ b/src/Explorer/Tree/DocumentId.ts @@ -1,5 +1,6 @@ import * as ko from "knockout"; import * as DataModels from "../../Contracts/DataModels"; +import { useDialog } from "../Controls/Dialog"; import DocumentsTab from "../Tabs/DocumentsTab"; export default class DocumentId { @@ -28,10 +29,20 @@ export default class DocumentId { } public click() { - if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) { + if (this.container.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Your unsaved changes will be lost. Do you want to continue?", + "OK", + () => this.loadDocument(), + "Cancel", + undefined + ); + } else { this.loadDocument(); } - return; } public partitionKeyHeader(): Object { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5b6dfe92..2130c01bb 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -24,6 +24,7 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; @@ -254,14 +255,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc label: "Delete", iconSrc: DeleteIcon, onClick: () => { - container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => container.deleteNotebookFile(item), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); }, }, { @@ -319,14 +322,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc label: "Delete", iconSrc: DeleteIcon, onClick: () => { - container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => container.deleteNotebookFile(item), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); }, }, { diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 6a00e92e9..6a9352db2 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -27,6 +27,7 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; @@ -712,14 +713,16 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Delete", iconSrc: DeleteIcon, onClick: () => { - this.container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined + ); }, }, { @@ -777,14 +780,16 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Delete", iconSrc: DeleteIcon, onClick: () => { - this.container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined + ); }, }, { diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index 8e6f77ece..215035ffe 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -8,6 +8,7 @@ import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { getErrorMessage } from "../Tables/Utilities"; import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab"; @@ -138,16 +139,21 @@ export default class StoredProcedure { } }; public delete() { - if (!window.confirm("Are you sure you want to delete the stored procedure?")) { - return; - } - - deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the stored procedure?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); }, - (reason) => {} + "Cancel", + undefined ); } diff --git a/src/Explorer/Tree/Trigger.ts b/src/Explorer/Tree/Trigger.ts index d90428b65..59d05de60 100644 --- a/src/Explorer/Tree/Trigger.ts +++ b/src/Explorer/Tree/Trigger.ts @@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import TriggerTab from "../Tabs/TriggerTab"; import { useSelectedNode } from "../useSelectedNode"; @@ -99,16 +100,21 @@ export default class Trigger { }; public delete() { - if (!window.confirm("Are you sure you want to delete the trigger?")) { - return; - } - - deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the trigger?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); }, - (reason) => {} + "Cancel", + undefined ); } } diff --git a/src/Explorer/Tree/UserDefinedFunction.ts b/src/Explorer/Tree/UserDefinedFunction.ts index 4264c0954..555f547bb 100644 --- a/src/Explorer/Tree/UserDefinedFunction.ts +++ b/src/Explorer/Tree/UserDefinedFunction.ts @@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; import { useSelectedNode } from "../useSelectedNode"; @@ -95,18 +96,23 @@ export default class UserDefinedFunction { } public delete() { - if (!window.confirm("Are you sure you want to delete the user defined function?")) { - return; - } - - deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the user defined function?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + () => { + /**/ + } + ); }, - () => { - /**/ - } + "Cancel", + undefined ); } } diff --git a/src/Utils/GalleryUtils.test.ts b/src/Utils/GalleryUtils.test.ts index b911fa352..4df3651cd 100644 --- a/src/Utils/GalleryUtils.test.ts +++ b/src/Utils/GalleryUtils.test.ts @@ -1,8 +1,9 @@ -import * as GalleryUtils from "./GalleryUtils"; -import { JunoClient, IGalleryItem } from "../Juno/JunoClient"; import { HttpStatusCodes } from "../Common/Constants"; +import { useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; +import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; +import * as GalleryUtils from "./GalleryUtils"; const galleryItem: IGalleryItem = { id: "id", @@ -29,11 +30,11 @@ describe("GalleryUtils", () => { it("downloadItem shows dialog in data explorer", () => { const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); - expect(container.showOkCancelModalDialog).toBeCalled(); + expect(useDialog.getState().visible).toBe(true); + expect(useDialog.getState().dialogProps).toBeDefined(); + expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks"); }); it("favoriteItem favorites item", async () => { @@ -66,11 +67,11 @@ describe("GalleryUtils", () => { it("deleteItem shows dialog in data explorer", () => { const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); - expect(container.showOkCancelModalDialog).toBeCalled(); + expect(useDialog.getState().visible).toBe(true); + expect(useDialog.getState().dialogProps).toBeDefined(); + expect(useDialog.getState().dialogProps.title).toBe("Remove published notebook"); }); it("getGalleryViewerProps gets gallery viewer props correctly", () => { diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index d9a52d348..16a41c6d3 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -3,7 +3,7 @@ import { Notebook } from "@nteract/commutable"; import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { HttpStatusCodes } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; -import { TextFieldProps } from "../Explorer/Controls/Dialog"; +import { TextFieldProps, useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, GalleryViewerComponent, @@ -222,7 +222,7 @@ export function downloadItem( }); const name = data.name; - container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( "Download to My Notebooks", `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", @@ -388,7 +388,7 @@ export function deleteItem( if (container) { trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id }); - container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( "Remove published notebook", `Would you like to remove ${data.name} from the gallery?`, "Remove", diff --git a/tsconfig.strict.json b/tsconfig.strict.json index d48357cda..ee037db87 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -37,7 +37,6 @@ "./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SubscriptionType.ts", "./src/Contracts/Versions.ts", - "./src/Explorer/Controls/Dialog.tsx", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", From 56699ccb1b13d88d66cc0832afa2404754e94055 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 30 Jul 2021 16:23:36 -0700 Subject: [PATCH 04/60] Fix new resource tree (#962) --- src/Common/ResourceTreeContainer.tsx | 6 +- src/Explorer/Explorer.tsx | 27 +++++--- .../Notebook/NotebookContentClient.ts | 31 +++++++-- src/Explorer/Notebook/useNotebook.ts | 30 ++++++--- .../CopyNotebookPane/CopyNotebookPane.tsx | 7 +- src/Explorer/Tree/ResourceTree.tsx | 67 ++++++++++++------- src/Platform/Hosted/extractFeatures.ts | 4 +- 7 files changed, 117 insertions(+), 55 deletions(-) diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index ca41610da..18a769b12 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -54,10 +54,10 @@ export const ResourceTreeContainer: FunctionComponent {userContext.authType === AuthType.ResourceToken ? ( - ) : userContext.features.enableReactResourceTree ? ( - - ) : ( + ) : userContext.features.enableKoResourceTree ? (
+ ) : ( + )}
{/* Collections Window - End */} diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index a4cd403f0..d854278c2 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -537,17 +537,22 @@ export default class Explorer { } } - public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { + public uploadFile( + name: string, + content: string, + parent: NotebookContentItem, + isGithubTree?: boolean + ): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to upload notebook, but notebook is not enabled"; handleError(error, "Explorer/uploadFile"); throw new Error(error); } - const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); + const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); promise .then(() => this.resourceTree.triggerRender()) - .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", reason)); + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); return promise; } @@ -672,7 +677,7 @@ export default class Explorer { return true; } - public renameNotebook(notebookFile: NotebookContentItem): void { + public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to rename notebook, but notebook is not enabled"; handleError(error, "Explorer/renameNotebook"); @@ -705,7 +710,7 @@ export default class Explorer { paneTitle="Rename Notebook" defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) + this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) } notebookFile={notebookFile} /> @@ -713,7 +718,7 @@ export default class Explorer { } } - public onCreateDirectory(parent: NotebookContentItem): void { + public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create notebook directory, but notebook is not enabled"; handleError(error, "Explorer/onCreateDirectory"); @@ -735,7 +740,7 @@ export default class Explorer { submitButtonLabel="Create" defaultInput="" onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input) + this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) } notebookFile={parent} /> @@ -804,7 +809,7 @@ export default class Explorer { } }; - public deleteNotebookFile(item: NotebookContentItem): Promise { + public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to delete notebook file, but notebook is not enabled"; handleError(error, "Explorer/deleteNotebookFile"); @@ -837,7 +842,7 @@ export default class Explorer { return Promise.reject(); } - return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( + return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( () => logConsoleInfo(`Successfully deleted: ${item.path}`), (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`) ); @@ -846,7 +851,7 @@ export default class Explorer { /** * This creates a new notebook file, then opens the notebook */ - public onNewNotebookClicked(parent?: NotebookContentItem): void { + public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create new notebook, but notebook is not enabled"; handleError(error, "Explorer/onNewNotebookClicked"); @@ -861,7 +866,7 @@ export default class Explorer { }); this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent) + .createNewNotebookFile(parent, isGithubTree) .then((newFile: NotebookContentItem) => { logConsoleInfo(`Successfully created: ${newFile.name}`); TelemetryProcessor.traceSuccess( diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index 3599c009c..5ca408c2a 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -36,7 +36,7 @@ export class NotebookContentClient { * * @param parent parent folder */ - public createNewNotebookFile(parent: NotebookContentItem): Promise { + public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); } @@ -57,6 +57,8 @@ export class NotebookContentClient { const notebookFile = xhr.response; const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed if (parent.children) { item.parent = parent; parent.children.push(item); @@ -66,9 +68,9 @@ export class NotebookContentClient { }); } - public async deleteContentItem(item: NotebookContentItem): Promise { + public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise { const path = await this.deleteNotebookFile(item.path); - useNotebook.getState().deleteNotebookItem(item); + useNotebook.getState().deleteNotebookItem(item, isGithubTree); // TODO: Delete once old resource tree is removed if (!path || path !== item.path) { @@ -91,7 +93,8 @@ export class NotebookContentClient { public async uploadFileAsync( name: string, content: string, - parent: NotebookContentItem + parent: NotebookContentItem, + isGithubTree?: boolean ): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); @@ -115,6 +118,8 @@ export class NotebookContentClient { .then((xhr: AjaxResponse) => { const notebookFile = xhr.response; const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed if (parent.children) { item.parent = parent; parent.children.push(item); @@ -137,7 +142,11 @@ export class NotebookContentClient { * @param sourcePath * @param targetName is not prefixed with path */ - public renameNotebook(item: NotebookContentItem, targetName: string): Promise { + public renameNotebook( + item: NotebookContentItem, + targetName: string, + isGithubTree?: boolean + ): Promise { const sourcePath = item.path; // Match extension if (sourcePath.indexOf(".") !== -1) { @@ -163,6 +172,9 @@ export class NotebookContentClient { item.name = notebookFile.name; item.path = notebookFile.path; item.timestamp = NotebookUtil.getCurrentTimestamp(); + + useNotebook.getState().updateNotebookItem(item, isGithubTree); + return item; }); } @@ -172,7 +184,11 @@ export class NotebookContentClient { * @param parent * @param newDirectoryName basename of the new directory */ - public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise { + public async createDirectory( + parent: NotebookContentItem, + newDirectoryName: string, + isGithubTree?: boolean + ): Promise { if (parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent is not a directory: ${parent.path}`); } @@ -199,8 +215,11 @@ export class NotebookContentClient { const dir = xhr.response; const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); + useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree); + // TODO: delete when ResourceTreeAdapter is removed item.parent = parent; parent.children?.push(item); + return item; }); } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index af1b47477..5a2c97b3e 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -38,8 +38,9 @@ interface NotebookState { setNotebookBasePath: (notebookBasePath: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; - updateNotebookItem: (item: NotebookContentItem) => void; - deleteNotebookItem: (item: NotebookContentItem) => void; + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; + updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; + deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; } @@ -141,19 +142,30 @@ export const useNotebook: UseStore = create((set, get) => ({ return undefined; }, - updateNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); + const parentItem = get().findItem(root, parent); + item.parent = parentItem; + if (parentItem.children) { + parentItem.children.push(item); + } else { + parentItem.children = [item]; + } + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); + }, + updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children.push(item); item.parent = parentItem; - set({ myNotebooksContentRoot: root }); + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, - deleteNotebookItem: (item: NotebookContentItem): void => { - const root = cloneDeep(get().myNotebooksContentRoot); + deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { + const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); - set({ myNotebooksContentRoot: root }); + isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { const myNotebooksContentRoot = { @@ -216,6 +228,7 @@ export const useNotebook: UseStore = create((set, get) => ({ path: "PsuedoDir", type: NotebookContentItemType.Directory, children: [], + parent: gitHubNotebooksContentRoot, }; pinnedRepo.branches.forEach((branch) => { @@ -223,6 +236,7 @@ export const useNotebook: UseStore = create((set, get) => ({ name: branch.name, path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), type: NotebookContentItemType.Directory, + parent: repoTreeItem, }); }); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 60cb731b8..3c63c00ae 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -98,6 +98,7 @@ export const CopyNotebookPane: FunctionComponent = ({ const copyNotebook = async (location: Location): Promise => { let parent: NotebookContentItem; + let isGithubTree: boolean; switch (location.type) { case "MyNotebooks": parent = { @@ -105,21 +106,23 @@ export const CopyNotebookPane: FunctionComponent = ({ path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; + isGithubTree = false; break; case "GitHub": parent = { - name: ResourceTreeAdapter.GitHubReposTitle, + name: selectedLocation.branch, path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), type: NotebookContentItemType.Directory, }; + isGithubTree = true; break; default: throw new Error(`Unsupported location type ${location.type}`); } - return container.uploadFile(name, content, parent); + return container.uploadFile(name, content, parent, isGithubTree); }; const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 2130c01bb..b8dae8259 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,5 +1,6 @@ import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import * as React from "react"; +import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import DeleteIcon from "../../../images/delete.svg"; import GalleryIcon from "../../../images/GalleryIcon.svg"; @@ -55,7 +56,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc galleryContentRoot, gitHubNotebooksContentRoot, updateNotebookItem, - } = useNotebook(); + } = useNotebook( + (state) => ({ + isNotebookEnabled: state.isNotebookEnabled, + myNotebooksContentRoot: state.myNotebooksContentRoot, + galleryContentRoot: state.galleryContentRoot, + gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, + updateNotebookItem: state.updateNotebookItem, + }), + shallow + ); const { activeTab, refreshActiveTab } = useTabs(); const showScriptNodes = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const pseudoDirPath = "PsuedoDir"; @@ -166,7 +176,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); } }); - } + }, + true ); gitHubNotebooksTree.contextMenu = [ @@ -202,9 +213,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc }; const buildChildNodes = ( - container: Explorer, item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode[] => { if (!item || !item.children) { return []; @@ -212,8 +223,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc return item.children.map((item) => { const result = item.type === NotebookContentItemType.Directory - ? buildNotebookDirectoryNode(item, onFileClick) - : buildNotebookFileNode(item, onFileClick); + ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) + : buildNotebookFileNode(item, onFileClick, isGithubTree); result.timestamp = item.timestamp; return result; }); @@ -222,7 +233,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc const buildNotebookFileNode = ( item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode => { return { label: item.name, @@ -239,17 +251,21 @@ export const ResourceTree: React.FC = ({ container }: Resourc (activeTab as any).notebookPath() === item.path ); }, - contextMenu: createFileContextMenu(container, item), + contextMenu: createFileContextMenu(container, item, isGithubTree), data: item, }; }; - const createFileContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + const createFileContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean + ): TreeNodeMenuItem[] => { let items: TreeNodeMenuItem[] = [ { label: "Rename", iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item), + onClick: () => container.renameNotebook(item, isGithubTree), }, { label: "Delete", @@ -261,7 +277,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc "Confirm delete", `Are you sure you want to delete "${item.name}"`, "Delete", - () => container.deleteNotebookFile(item), + () => container.deleteNotebookFile(item, isGithubTree), "Cancel", undefined ); @@ -311,12 +327,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc } }; - const createDirectoryContextMenu = (container: Explorer, item: NotebookContentItem): TreeNodeMenuItem[] => { + const createDirectoryContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean + ): TreeNodeMenuItem[] => { let items: TreeNodeMenuItem[] = [ { label: "Refresh", iconSrc: RefreshIcon, - onClick: () => loadSubitems(item), + onClick: () => loadSubitems(item, isGithubTree), }, { label: "Delete", @@ -328,7 +348,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc "Confirm delete", `Are you sure you want to delete "${item.name}?"`, "Delete", - () => container.deleteNotebookFile(item), + () => container.deleteNotebookFile(item, isGithubTree), "Cancel", undefined ); @@ -337,17 +357,17 @@ export const ResourceTree: React.FC = ({ container }: Resourc { label: "Rename", iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item), + onClick: () => container.renameNotebook(item, isGithubTree), }, { label: "New Directory", iconSrc: NewNotebookIcon, - onClick: () => container.onCreateDirectory(item), + onClick: () => container.onCreateDirectory(item, isGithubTree), }, { label: "New Notebook", iconSrc: NewNotebookIcon, - onClick: () => container.onNewNotebookClicked(item), + onClick: () => container.onNewNotebookClicked(item, isGithubTree), }, { label: "Upload File", @@ -372,7 +392,8 @@ export const ResourceTree: React.FC = ({ container }: Resourc const buildNotebookDirectoryNode = ( item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean ): TreeNode => { return { label: item.name, @@ -382,7 +403,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc isLeavesParentsSeparate: true, onClick: () => { if (!item.children) { - loadSubitems(item); + loadSubitems(item, isGithubTree); } }, isSelected: () => { @@ -395,9 +416,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc (activeTab as any).notebookPath() === item.path ); }, - contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item) : undefined, + contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, data: item, - children: buildChildNodes(container, item, onFileClick), + children: buildChildNodes(item, onFileClick, isGithubTree), }; }; @@ -699,9 +720,9 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; - const loadSubitems = async (item: NotebookContentItem): Promise => { + const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); - updateNotebookItem(updatedItem); + updateNotebookItem(updatedItem, isGithubTree); }; const dataRootNode = buildDataTree(); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 4a85dca8d..c7ea359ab 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -16,7 +16,7 @@ export type Features = { readonly enableTtl: boolean; readonly executeSproc: boolean; readonly enableAadDataPlane: boolean; - readonly enableReactResourceTree: boolean; + readonly enableKoResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -58,7 +58,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), - enableReactResourceTree: "true" === get("enablereactresourcetree"), + enableKoResourceTree: "true" === get("enablekoresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), junoEndpoint: get("junoendpoint"), From b65456f754da964581ee96e13cb1f967245d7a84 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Mon, 2 Aug 2021 18:54:30 -0700 Subject: [PATCH 05/60] Fix bug in Dialog State store (#969) --- src/Explorer/Controls/Dialog.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index d5e308d69..449627d13 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -41,7 +41,16 @@ export const useDialog: UseStore = create((set, get) => ({ visible: false, openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), closeDialog: () => - set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), + set( + (state) => ({ + visible: false, + openDialog: state.openDialog, + closeDialog: state.closeDialog, + showOkCancelModalDialog: state.showOkCancelModalDialog, + showOkModalDialog: state.showOkModalDialog, + }), + true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above + ), showOkCancelModalDialog: ( title: string, subText: string, From ee4404c439efa3e5d97e0c3e61675d5017959f6c Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Mon, 2 Aug 2021 19:11:42 -0700 Subject: [PATCH 06/60] Fix enable synapse link error (#918) * Fix enable synapse error * Default all ARM requests to JSON Co-authored-by: Steve Faulkner --- src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts | 8 +++++++- src/Utils/arm/request.ts | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts index 87b00b24b..5c52c7d61 100644 --- a/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts +++ b/src/Utils/arm/generatedClients/cosmos/databaseAccounts.ts @@ -29,7 +29,13 @@ export async function update( body: Types.DatabaseAccountUpdateParameters ): Promise { const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; - return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body }); + return armRequest({ + host: configContext.ARM_ENDPOINT, + path, + method: "PATCH", + apiVersion, + body, + }); } /* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */ diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts index e593633ac..69391201b 100644 --- a/src/Utils/arm/request.ts +++ b/src/Utils/arm/request.ts @@ -6,6 +6,7 @@ Instead, generate ARM clients that consume this function with stricter typing. */ import promiseRetry, { AbortError } from "p-retry"; +import { HttpHeaders } from "../../Common/Constants"; import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; @@ -45,6 +46,7 @@ interface Options { method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"; body?: unknown; queryParams?: ARMQueryParams; + contentType?: string; } export async function armRequestWithoutPolling({ @@ -54,6 +56,7 @@ export async function armRequestWithoutPolling({ method, body: requestBody, queryParams, + contentType, }: Options): Promise<{ result: T; operationStatusUrl: string }> { const url = new URL(path, host); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); @@ -70,6 +73,7 @@ export async function armRequestWithoutPolling({ method, headers: { Authorization: userContext.authorizationToken, + [HttpHeaders.contentType]: contentType || "application/json", }, body: requestBody ? JSON.stringify(requestBody) : undefined, }); @@ -104,6 +108,7 @@ export async function armRequest({ method, body: requestBody, queryParams, + contentType, }: Options): Promise { const armRequestResult = await armRequestWithoutPolling({ host, @@ -112,6 +117,7 @@ export async function armRequest({ method, body: requestBody, queryParams, + contentType, }); const operationStatusUrl = armRequestResult.operationStatusUrl; if (operationStatusUrl) { From 51f3f9a718f39cdb2f17053ff201351dbb204c0b Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Wed, 4 Aug 2021 20:16:18 +0530 Subject: [PATCH 07/60] Move inputTypeahead to react (#946) Co-authored-by: Steve Faulkner --- .../InputTypeahead/InputTypeahead.less | 12 +- .../InputTypeaheadComponent.tsx | 242 +++++++----------- .../InputTypeaheadComponent.test.tsx.snap | 78 +++--- 3 files changed, 139 insertions(+), 193 deletions(-) diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less index e56bfaa51..1d68e3b7e 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less +++ b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less @@ -5,6 +5,9 @@ display: inline-block; width: 100%; + .input-type-head-text-field { + width: 100%; + } textarea { width: 100%; line-height: 1; @@ -21,4 +24,11 @@ } } } - +.input-typeahead-chocies-container { + border: 1px solid lightgrey; + padding: 5px 10px 5px 10px; + cursor: pointer; + .choice-caption{ + font-size: 14px; + } +} \ No newline at end of file diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx index 69f01a745..2e9a2d104 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx +++ b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx @@ -6,14 +6,13 @@ * typeaheadOverrideOptions: { dynamic:false } * */ -import "jquery-typeahead"; +import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react"; import * as React from "react"; -import { KeyCodes } from "../../../Common/Constants"; import "./InputTypeahead.less"; export interface Item { caption: string; - value: any; + value: string; } /** @@ -75,170 +74,125 @@ export interface InputTypeaheadComponentProps { useTextarea?: boolean; } -interface OnClickItem { - matchedKey: string; - value: any; - caption: string; - group: string; +interface InputTypeaheadComponentState { + isSuggestionVisible: boolean; + selectedChoice: Item; + filteredChoices: Item[]; } -interface Cache { - inputValue: string; - selection: Item; -} - -interface InputTypeaheadComponentState {} - export class InputTypeaheadComponent extends React.Component< InputTypeaheadComponentProps, InputTypeaheadComponentState > { - private inputElt: HTMLElement; - private containerElt: HTMLElement; - - private cache: Cache; - private inputValue: string; - private selection: Item; - - public constructor(props: InputTypeaheadComponentProps) { + constructor(props: InputTypeaheadComponentProps) { super(props); - this.cache = { - inputValue: null, - selection: null, + this.state = { + isSuggestionVisible: false, + filteredChoices: [], + selectedChoice: { + caption: "", + value: "", + }, }; } - /** - * Props have changed - * @param prevProps - * @param prevState - * @param snapshot - */ - public componentDidUpdate( - prevProps: InputTypeaheadComponentProps, - prevState: InputTypeaheadComponentState, - snapshot: any - ): void { - if (prevProps.defaultValue !== this.props.defaultValue) { - $(this.inputElt).val(this.props.defaultValue); - this.initializeTypeahead(); - } - } - - /** - * Executed once react is done building the DOM for this component - */ - public componentDidMount(): void { - this.initializeTypeahead(); - } - - public render(): JSX.Element { + private onRenderCell = (item: Item): JSX.Element => { return ( - -
) => this.onKeyDown(event)} - > -
(this.containerElt = input)}> -
- - {this.props.useTextarea ? ( -