diff --git a/.eslintignore b/.eslintignore index 78e9b517e..20cdeb4c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -89,10 +89,7 @@ src/Explorer/Tables/TableEntityProcessor.ts src/Explorer/Tables/Utilities.ts src/Explorer/Tabs/ConflictsTab.ts src/Explorer/Tabs/DatabaseSettingsTab.ts -src/Explorer/Tabs/DocumentsTab.test.ts -src/Explorer/Tabs/DocumentsTab.ts src/Explorer/Tabs/GraphTab.ts -src/Explorer/Tabs/MongoDocumentsTab.ts src/Explorer/Tabs/NotebookV2Tab.ts src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/TabComponents.ts diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index a4834b128..b56b78bb8 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -3,8 +3,7 @@ import * as _ from "underscore"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; -import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; -import DocumentId from "../Explorer/Tree/DocumentId"; +import DocumentId, { IDocumentIdContainer } from "../Explorer/Tree/DocumentId"; import { useDatabases } from "../Explorer/useDatabases"; import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; @@ -162,10 +161,10 @@ export class QueriesClient { { partitionKey: QueriesClient.PartitionKey, partitionKeyProperties: ["id"], - } as DocumentsTab, + } as IDocumentIdContainer, query, [query.queryName], - ); // TODO: Remove DocumentId's dependency on DocumentsTab + ); const options: any = { partitionKey: query.resourceId }; return deleteDocument(queriesCollection, documentId) .then( diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html deleted file mode 100644 index cfe1b2039..000000000 --- a/src/Explorer/Tabs/DocumentsTab.html +++ /dev/null @@ -1,239 +0,0 @@ -
- - -
-
-

Title

-
Text
-
-
- - -
-
-
-
- - - - -
- -
- SELECT * FROM c - - -
-
- Filter : - - No filter applied - - -
- - - -
-
-
- SELECT * FROM c - - - - - - - - - - - - - - Hide filter - -
-
-
- -
- - - -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- Refresh documents -
- -
-
-
- Load more -
- - -
-
-
-
-

Document WaterMark

-

Create new or work with existing document(s).

-
- - - -
- -
diff --git a/src/Explorer/Tabs/DocumentsTab.test.ts b/src/Explorer/Tabs/DocumentsTab.test.ts deleted file mode 100644 index c8d84e716..000000000 --- a/src/Explorer/Tabs/DocumentsTab.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as ko from "knockout"; -import { DatabaseAccount } from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { updateUserContext } from "../../UserContext"; -import Explorer from "../Explorer"; -import DocumentId from "../Tree/DocumentId"; -import DocumentsTab from "./DocumentsTab"; - -describe("Documents tab", () => { - describe("buildQuery", () => { - it("should generate the right select query for SQL API", () => { - const documentsTab = new DocumentsTab({ - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.buildQuery("")).toContain("select"); - }); - }); - - describe("showPartitionKey", () => { - const explorer = new Explorer(); - const mongoExplorer = new Explorer(); - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableGremlin" }], - }, - } as DatabaseAccount, - }); - - const collectionWithoutPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - container: explorer, - }); - - const collectionWithSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: true, - }, - container: explorer, - }); - - const collectionWithNonSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: false, - }, - container: explorer, - }); - - const mongoCollectionWithSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: true, - }, - container: mongoExplorer, - }); - - it("should be false for null or undefined collection", () => { - const documentsTab = new DocumentsTab({ - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be false for null or undefined partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithoutPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be true for non-Mongo accounts with system partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(true); - }); - - it("should be false for Mongo accounts with system partitionKey", () => { - updateUserContext({ - apiType: "Mongo", - }); - const documentsTab = new DocumentsTab({ - collection: mongoCollectionWithSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be true for non-system partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithNonSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(true); - }); - }); -}); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts deleted file mode 100644 index 949b13697..000000000 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ /dev/null @@ -1,1080 +0,0 @@ -import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; -import { Platform, configContext } from "ConfigContext"; -import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; -import { KeyboardAction, KeyboardActionGroup, KeyboardHandlerSetter, useKeyboardActionGroup } from "KeyboardShortcuts"; -import { QueryConstants } from "Shared/Constants"; -import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; -import * as ko from "knockout"; -import Q from "q"; -import { format } from "react-string-format"; -import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; -import NewDocumentIcon from "../../../images/NewDocument.svg"; -import UploadIcon from "../../../images/Upload_16x16.svg"; -import DiscardIcon from "../../../images/discard.svg"; -import SaveIcon from "../../../images/save-cosmos.svg"; -import * as Constants from "../../Common/Constants"; -import { - DocumentsGridMetrics, - KeyCodes, - QueryCopilotSampleContainerId, - QueryCopilotSampleDatabaseId, -} from "../../Common/Constants"; -import editable from "../../Common/EditableUtility"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; -import { createDocument } from "../../Common/dataAccess/createDocument"; -import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; -import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; -import { readDocument } from "../../Common/dataAccess/readDocument"; -import { updateDocument } from "../../Common/dataAccess/updateDocument"; -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 { userContext } from "../../UserContext"; -import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; -import * as QueryUtils from "../../Utils/QueryUtils"; -import { extractPartitionKeyValues } 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"; -import { useSelectedNode } from "../useSelectedNode"; -import template from "./DocumentsTab.html"; -import TabsBase from "./TabsBase"; - -export default class DocumentsTab extends TabsBase { - public readonly html = template; - public selectedDocumentId: ko.Observable; - public selectedDocumentContent: ViewModels.Editable; - public initialDocumentContent: ko.Observable; - public documentContentsGridId: string; - public documentContentsContainerId: string; - public filterContent: ko.Observable; - public appliedFilter: ko.Observable; - public lastFilterContents: ko.ObservableArray; - public isFilterExpanded: ko.Observable; - public isFilterCreated: ko.Observable; - public applyFilterButton: ViewModels.Button; - public isEditorDirty: ko.Computed; - public editorState: ko.Observable; - public newDocumentButton: ViewModels.Button; - public saveNewDocumentButton: ViewModels.Button; - public saveExistingDocumentButton: ViewModels.Button; - public discardNewDocumentChangesButton: ViewModels.Button; - public discardExisitingDocumentChangesButton: ViewModels.Button; - public deleteExisitingDocumentButton: ViewModels.Button; - public displayedError: ko.Observable; - public accessibleDocumentList: AccessibleVerticalList; - public dataContentsGridScrollHeight: ko.Observable; - public isPreferredApiMongoDB: boolean; - public shouldShowEditor: ko.Computed; - public splitter: Splitter; - public showPartitionKey: boolean; - public idHeader: string; - - // TODO need to refactor - public partitionKey: DataModels.PartitionKey; - public partitionKeyPropertyHeaders: string[]; - public partitionKeyProperties: string[]; - public documentIds: ko.ObservableArray; - - private _documentsIterator: QueryIterator; - private _resourceTokenPartitionKey: string; - private _isQueryCopilotSampleContainer: boolean; - private queryAbortController: AbortController; - private cancelQueryTimeoutID: NodeJS.Timeout; - private setKeyboardActions: KeyboardHandlerSetter; - - constructor(options: ViewModels.DocumentsTabOptions) { - super(options); - this.setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); - this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB; - - this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; - - this.documentContentsGridId = `documentContentsGrid${this.tabId}`; - this.documentContentsContainerId = `documentContentsContainer${this.tabId}`; - this.editorState = ko.observable( - ViewModels.DocumentExplorerState.noDocumentSelected, - ); - this.selectedDocumentId = ko.observable(); - this.selectedDocumentContent = editable.observable(""); - this.initialDocumentContent = ko.observable(""); - this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey); - this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; - this.documentIds = options.documentIds; - - this.partitionKeyPropertyHeaders = this.collection?.partitionKeyPropertyHeaders || this.partitionKey?.paths; - this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => - partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), - ); - - this.isFilterExpanded = ko.observable(false); - this.isFilterCreated = ko.observable(true); - this.filterContent = ko.observable(""); - this.appliedFilter = ko.observable(""); - this.displayedError = ko.observable(""); - this.lastFilterContents = ko.observableArray([ - 'WHERE c.id = "foo"', - "ORDER BY c._ts DESC", - 'WHERE c.id = "foo" ORDER BY c._ts DESC', - ]); - - this.dataContentsGridScrollHeight = ko.observable(null); - - // initialize splitter only after template has been loaded so dom elements are accessible - super.onTemplateReady((isTemplateReady: boolean) => { - if (isTemplateReady) { - const tabContainer: HTMLElement = document.getElementById("content"); - const splitterBounds: SplitterBounds = { - min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth, - max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth, - }; - this.splitter = new Splitter({ - splitterId: "h_splitter2", - leftId: this.documentContentsContainerId, - bounds: splitterBounds, - direction: SplitterDirection.Vertical, - }); - } - }); - - this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds()); - this.accessibleDocumentList.setOnSelect( - (selectedDocument: DocumentId) => selectedDocument && selectedDocument.click(), - ); - this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) => - this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId), - ); - this.documentIds.subscribe((newDocuments: DocumentId[]) => { - this.accessibleDocumentList.updateItemList(newDocuments); - if (newDocuments.length > 0) { - this.dataContentsGridScrollHeight( - newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", - ); - } else { - this.dataContentsGridScrollHeight( - DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", - ); - } - }); - - this.isEditorDirty = ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - return false; - - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - return true; - - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return ( - this.selectedDocumentContent.getEditableOriginalValue() !== - this.selectedDocumentContent.getEditableCurrentValue() - ); - - default: - return false; - } - }); - - this.newDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - return true; - }), - }; - - this.saveNewDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }), - }; - - this.discardNewDocumentChangesButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }), - }; - - this.saveExistingDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - }; - - this.discardExisitingDocumentChangesButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - }; - - this.deleteExisitingDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - }; - - this.applyFilterButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }), - }; - this.buildCommandBarOptions(); - this.shouldShowEditor = ko.computed(() => { - const documentHasContent: boolean = - this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0; - const isNewDocument: boolean = - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid; - - return documentHasContent || isNewDocument; - }); - this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent)); - - this.showPartitionKey = this._shouldShowPartitionKey(); - this._isQueryCopilotSampleContainer = - this.collection?.isSampleCollection && - this.collection?.databaseId === QueryCopilotSampleDatabaseId && - this.collection?.id() === QueryCopilotSampleContainerId; - } - - private _shouldShowPartitionKey(): boolean { - if (!this.collection) { - return false; - } - - if (!this.collection.partitionKey) { - return false; - } - - if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) { - return false; - } - - return true; - } - - /** - * Query first page of documents - * Select and query first document and display content - */ - private async autoPopulateContent(applyFilterButtonPressed?: boolean) { - // reset iterator - this._documentsIterator = this.createIterator(); - // load documents - await this.loadNextPage(applyFilterButtonPressed); - - // Select first document and load content - if (this.documentIds().length > 0) { - this.documentIds()[0].click(); - } - } - - public onShowFilterClick(): Q.Promise { - this.isFilterCreated(true); - this.isFilterExpanded(true); - - $(".filterDocExpanded").addClass("active"); - $("#content").addClass("active"); - $(".querydropdown").focus(); - - return Q(); - } - - public onHideFilterClick(): Q.Promise { - this.isFilterExpanded(false); - - $(".filterDocExpanded").removeClass("active"); - $("#content").removeClass("active"); - $(".queryButton").focus(); - return Q(); - } - - public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.onHideFilterClick(); - event.stopPropagation(); - return false; - } - return true; - }; - - public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise { - // clear documents grid - this.documentIds([]); - try { - // reset iterator - this._documentsIterator = this.createIterator(); - // load documents - await this.autoPopulateContent(applyFilterButtonPressed); - // collapse filter - this.appliedFilter(this.filterContent()); - this.isFilterExpanded(false); - document.getElementById("errorStatusIcon")?.focus(); - } catch (error) { - useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); - } - } - - public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.refreshDocumentsGrid(); - event.stopPropagation(); - return false; - } - return true; - }; - - public onAbortQueryClick(): void { - this.queryAbortController.abort(); - } - - /** - * TODO Doesn't seem to be used: remove? - * @param clickedDocumentId - * @returns - */ - public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise { - if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) { - return Q(); - } - - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - - 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(); - } - }; - - private initializeNewDocument = (): void => { - this.selectedDocumentId(null); - const newDocument: any = { - id: "replace_with_new_document_id", - }; - this.partitionKeyProperties.forEach((partitionKeyProperty) => { - let target = newDocument; - const keySegments = partitionKeyProperty.split("."); - const finalSegment = keySegments.pop(); - - // Initialize nested objects as needed - keySegments.forEach((segment) => { - target = target[segment] = target[segment] || {}; - }); - - target[finalSegment] = "replace_with_new_partition_key_value"; - }); - const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4); - this.initialDocumentContent(defaultDocument); - this.selectedDocumentContent.setBaseline(defaultDocument); - this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - }; - - public onSaveNewDocumentClick = (): Promise => { - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - const document = JSON.parse(this.selectedDocumentContent()); - this.isExecuting(true); - return createDocument(this.collection, document) - .then( - (savedDocument: any) => { - const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - this.initialDocumentContent(value); - const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( - savedDocument, - this.partitionKey as PartitionKeyDefinition, - ); - let id = new DocumentId(this, savedDocument, partitionKeyValueArray); - let ids = this.documentIds(); - ids.push(id); - - this.selectedDocumentId(id); - this.documentIds(ids); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Create document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public onRevertNewDocumentClick = (): Q.Promise => { - this.initialDocumentContent(""); - this.selectedDocumentContent(""); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - - return Q(); - }; - - public onSaveExistingDocumentClick = (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const documentContent = JSON.parse(this.selectedDocumentContent()); - - const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( - documentContent, - this.partitionKey as PartitionKeyDefinition, - ); - selectedDocumentId.partitionKeyValue = partitionKeyValueArray; - - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - this.isExecuting(true); - return updateDocument(this.collection, selectedDocumentId, documentContent) - .then( - (updatedDocument: any) => { - const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - this.initialDocumentContent(value); - this.documentIds().forEach((documentId: DocumentId) => { - if (documentId.rid === updatedDocument._rid) { - documentId.id(updatedDocument.id); - } - }); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Update document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public onRevertExisitingDocumentClick = (): Q.Promise => { - this.selectedDocumentContent.setBaseline(this.initialDocumentContent()); - this.initialDocumentContent.valueHasMutated(); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - - return Q(); - }; - - public onDeleteExisitingDocumentClick = async (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const msg = !this.isPreferredApiMongoDB - ? "Are you sure you want to delete the selected item ?" - : "Are you sure you want to delete the selected document ?"; - - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - msg, - "Delete", - async () => await this._deleteDocument(selectedDocumentId), - "Cancel", - undefined, - ); - }; - - public onValidDocumentEdit(): Q.Promise { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid - ) { - this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - return Q(); - } - - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); - return Q(); - } - - public onInvalidDocumentEdit(): Q.Promise { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid - ) { - this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid); - return Q(); - } - - if ( - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid - ) { - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); - return Q(); - } - - return Q(); - } - - public onTabClick(): void { - super.onTabClick(); - 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 { - super.onActivate(); - - this.setKeyboardActions({ - [KeyboardAction.SEARCH]: () => { - this.onShowFilterClick(); - return true; - }, - [KeyboardAction.CLEAR_SEARCH]: () => { - this.filterContent(""); - this.refreshDocumentsGrid(true); - return true; - }, - }); - - if (!this._documentsIterator) { - try { - await this.autoPopulateContent(); - } catch (error) { - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - } - } - } - - protected __deleteDocument(documentId: DocumentId): Promise { - return deleteDocument(this.collection, documentId); - } - - private _deleteDocument(selectedDocumentId: DocumentId): Promise { - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - this.isExecuting(true); - return this.__deleteDocument(selectedDocumentId) - .then( - () => { - this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); - this.selectedDocumentContent(""); - this.selectedDocumentId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - TelemetryProcessor.traceSuccess( - Action.DeleteDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - console.error(error); - TelemetryProcessor.traceFailure( - Action.DeleteDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - } - - public createIterator(): QueryIterator { - this.queryAbortController = new AbortController(); - const filter: string = this.filterContent().trim(); - const query: string = this.buildQuery(filter); - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - - if (this._resourceTokenPartitionKey) { - options.partitionKey = this._resourceTokenPartitionKey; - } - options.abortSignal = this.queryAbortController.signal; - return this._isQueryCopilotSampleContainer - ? querySampleDocuments(query, options) - : queryDocuments(this.collection.databaseId, this.collection.id(), query, options); - } - - public async selectDocument(documentId: DocumentId): Promise { - this.selectedDocumentId(documentId); - const content = await (this._isQueryCopilotSampleContainer - ? readSampleDocument(documentId) - : readDocument(this.collection, documentId)); - this.initDocumentEditor(documentId, content); - } - - public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise { - this.isExecuting(true); - this.isExecutionError(false); - let automaticallyCancelQueryAfterTimeout: boolean; - if (applyFilterButtonClicked && this.queryTimeoutEnabled()) { - const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout); - automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean( - StorageKey.AutomaticallyCancelQueryAfterTimeout, - ); - const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => { - if (this.isExecuting()) { - if (automaticallyCancelQueryAfterTimeout) { - this.queryAbortController.abort(); - } else { - useDialog - .getState() - .showOkCancelModalDialog( - QueryConstants.CancelQueryTitle, - format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached), - "Yes", - () => this.queryAbortController.abort(), - "No", - undefined, - ); - } - } - }, queryTimeout); - this.cancelQueryTimeoutID = cancelQueryTimeoutID; - } - return this._loadNextPageInternal() - .then( - (documentsIdsResponse = []) => { - const currentDocuments = this.documentIds(); - const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); - const nextDocumentIds = documentsIdsResponse - // filter documents already loaded in observable - .filter((d: any) => { - return currentDocumentsRids.indexOf(d._rid) < 0; - }) - // map raw response to view model - .map((rawDocument: any) => { - const partitionKeyValue = rawDocument._partitionKeyValue; - return new DocumentId(this, rawDocument, partitionKeyValue); - }); - - const merged = currentDocuments.concat(nextDocumentIds); - this.documentIds(merged); - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - logConsoleError(errorMessage); - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - ) - .finally(() => { - this.isExecuting(false); - if (applyFilterButtonClicked && this.queryTimeoutEnabled()) { - clearTimeout(this.cancelQueryTimeoutID); - if (!automaticallyCancelQueryAfterTimeout) { - useDialog.getState().closeDialog(); - } - } - }); - } - - public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => { - if (event.key === " " || event.key === "Enter") { - const focusElement = document.getElementById(this.documentContentsGridId); - this.loadNextPage(); - focusElement && focusElement.focus(); - event.stopPropagation(); - event.preventDefault(); - } - }; - - protected _loadNextPageInternal(): Q.Promise { - return Q(this._documentsIterator.fetchNext().then((response) => response.resources)); - } - - protected _onEditorContentChange(newContent: string) { - try { - let parsed: any = JSON.parse(newContent); - this.onValidDocumentEdit(); - } catch (e) { - this.onInvalidDocumentEdit(); - } - } - - public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise { - if (documentId) { - const content: string = this.renderObjectForEditor(documentContent, null, 4); - this.selectedDocumentContent.setBaseline(content); - this.initialDocumentContent(content); - const newState = documentId - ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits - : ViewModels.DocumentExplorerState.newDocumentValid; - this.editorState(newState); - } - - return Q(); - } - - public buildQuery(filter: string): string { - return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperties, this.partitionKey); - } - - protected getTabsButtons(): CommandButtonComponentProps[] { - if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { - // All the following buttons require write access - return []; - } - - const buttons: CommandButtonComponentProps[] = []; - const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; - if (this.newDocumentButton.visible()) { - buttons.push({ - iconSrc: NewDocumentIcon, - iconAlt: label, - keyboardAction: KeyboardAction.NEW_ITEM, - onCommandClick: this.onNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(), - id: "mongoNewDocumentBtn", - }); - } - - if (this.saveNewDocumentButton.visible()) { - const label = "Save"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - keyboardAction: KeyboardAction.SAVE_ITEM, - onCommandClick: this.onSaveNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.saveNewDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.discardNewDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, - onCommandClick: this.onRevertNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.discardNewDocumentChangesButton.enabled() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.saveExistingDocumentButton.visible()) { - const label = "Update"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - keyboardAction: KeyboardAction.SAVE_ITEM, - onCommandClick: this.onSaveExistingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.saveExistingDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.discardExisitingDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, - onCommandClick: this.onRevertExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.discardExisitingDocumentChangesButton.enabled() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.deleteExisitingDocumentButton.visible()) { - const label = "Delete"; - buttons.push({ - iconSrc: DeleteDocumentIcon, - iconAlt: label, - keyboardAction: KeyboardAction.DELETE_ITEM, - onCommandClick: this.onDeleteExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.deleteExisitingDocumentButton.enabled() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (!this.isPreferredApiMongoDB) { - buttons.push(DocumentsTab._createUploadButton(this.collection.container)); - } - - return buttons; - } - - protected buildCommandBarOptions(): void { - ko.computed(() => - ko.toJSON([ - this.newDocumentButton.visible, - this.newDocumentButton.enabled, - this.saveNewDocumentButton.visible, - this.saveNewDocumentButton.enabled, - this.discardNewDocumentChangesButton.visible, - this.discardNewDocumentChangesButton.enabled, - this.saveExistingDocumentButton.visible, - this.saveExistingDocumentButton.enabled, - this.discardExisitingDocumentChangesButton.visible, - this.discardExisitingDocumentChangesButton.enabled, - this.deleteExisitingDocumentButton.visible, - this.deleteExisitingDocumentButton.enabled, - ]), - ).subscribe(() => this.updateNavbarWithTabsButtons()); - this.updateNavbarWithTabsButtons(); - } - - public static _createUploadButton(container: Explorer): CommandButtonComponentProps { - const label = "Upload Item"; - return { - id: "uploadItemBtn", - iconSrc: UploadIcon, - iconAlt: label, - onCommandClick: () => { - const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - selectedCollection && container.openUploadItemsPanePane(); - }, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: true, - disabled: - useSelectedNode.getState().isDatabaseNodeOrNoneSelected() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }; - } - - private queryTimeoutEnabled(): boolean { - return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled); - } -} diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index dd84cc976..ab39edda5 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -15,9 +15,9 @@ import { Platform, configContext } from "ConfigContext"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "Explorer/Controls/Dialog"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; -import DocumentsTab from "Explorer/Tabs/DocumentsTab"; import { getPlatformTheme } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { KeyboardAction } from "KeyboardShortcuts"; @@ -31,6 +31,7 @@ import { format } from "react-string-format"; import { CSSProperties } from "styled-components"; import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg"; import NewDocumentIcon from "../../../../images/NewDocument.svg"; +import UploadIcon from "../../../../images/Upload_16x16.svg"; import DiscardIcon from "../../../../images/discard.svg"; import SaveIcon from "../../../../images/save-cosmos.svg"; import * as Constants from "../../../Common/Constants"; @@ -228,6 +229,25 @@ type ButtonsDependencies = { onDeleteExistingDocumentsClick: UiKeyboardEvent; }; +const createUploadButton = (container: Explorer): CommandButtonComponentProps => { + const label = "Upload Item"; + return { + id: "uploadItemBtn", + iconSrc: UploadIcon, + iconAlt: label, + onCommandClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && container.openUploadItemsPanePane(); + }, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: + useSelectedNode.getState().isDatabaseNodeOrNoneSelected() || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + }; +}; + const getTabsButtons = ({ _collection, selectedRows, @@ -344,7 +364,7 @@ const getTabsButtons = ({ } if (!isPreferredApiMongoDB) { - buttons.push(DocumentsTab._createUploadButton(_collection.container)); + buttons.push(createUploadButton(_collection.container)); } return buttons; @@ -1671,12 +1691,12 @@ const DocumentsTabComponent: React.FunctionComponent<{
@@ -1769,9 +1789,9 @@ const DocumentsTabComponent: React.FunctionComponent<{ onClick={() => refreshDocumentsGrid(true)} disabled={!applyFilterButton.enabled} /* data-bind=" - click: refreshDocumentsGrid.bind($data, true), - enable: applyFilterButton.enabled" - */ + click: refreshDocumentsGrid.bind($data, true), + enable: applyFilterButton.enabled" + */ aria-label="Apply filter" tabIndex={0} > @@ -1784,9 +1804,9 @@ const DocumentsTabComponent: React.FunctionComponent<{ style={filterButtonStyle} appearance="primary" /* data-bind=" - visible: !isPreferredApiMongoDB && isExecuting, - click: onAbortQueryClick" - */ + visible: !isPreferredApiMongoDB && isExecuting, + click: onAbortQueryClick" + */ aria-label="Cancel Query" onClick={() => queryAbortController.abort()} tabIndex={0} diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts deleted file mode 100644 index d3bf31a54..000000000 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; -import { extractPartitionKeyValues } from "Utils/QueryUtils"; -import * as ko from "knockout"; -import Q from "q"; -import * as Constants from "../../Common/Constants"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as Logger from "../../Common/Logger"; -import { - createDocument, - deleteDocument, - queryDocuments, - readDocument, - updateDocument, -} from "../../Common/MongoProxyClient"; -import MongoUtility from "../../Common/MongoUtility"; -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"; - -export default class MongoDocumentsTab extends DocumentsTab { - public collection: ViewModels.Collection; - private continuationToken: string; - - constructor(options: ViewModels.DocumentsTabOptions) { - super(options); - this.lastFilterContents = ko.observableArray(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]); - - this.partitionKeyProperties = this.partitionKeyProperties?.map((partitionKeyProperty, i) => { - if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) { - partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); - } - - if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) { - // From $v.shard.$v.key.$v > shard.key - partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); - this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty; - } - - return partitionKeyProperty; - }); - - this.isFilterExpanded = ko.observable(true); - super.buildCommandBarOptions.bind(this); - super.buildCommandBarOptions(); - } - - public onSaveNewDocumentClick = (): Promise => { - const documentContent = JSON.parse(this.selectedDocumentContent()); - this.displayedError(""); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - const partitionKeyProperty = this.partitionKeyProperties?.[0]; - if (partitionKeyProperty !== "_id" && !this._hasShardKeySpecified(documentContent)) { - const message = `The document is lacking the shard property: ${partitionKeyProperty}`; - this.displayedError(message); - let that = this; - setTimeout(() => { - that.displayedError(""); - }, Constants.ClientDefaults.errorNotificationTimeoutMs); - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: message, - }, - startKey, - ); - Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); - throw new Error("Document without shard key"); - } - - this.isExecutionError(false); - this.isExecuting(true); - return createDocument( - this.collection.databaseId, - this.collection, - this.partitionKeyProperties?.[0], - documentContent, - ) - .then( - (savedDocument: any) => { - const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( - savedDocument, - this._getPartitionKeyDefinition() as PartitionKeyDefinition, - ); - - let id = new ObjectId(this, savedDocument, partitionKeyArray); - let ids = this.documentIds(); - ids.push(id); - delete savedDocument._self; - - let value: string = this.renderObjectForEditor(savedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - - this.selectedDocumentId(id); - this.documentIds(ids); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Create document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public onSaveExistingDocumentClick = (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const documentContent = this.selectedDocumentContent(); - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent) - .then( - (updatedDocument: any) => { - let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - - this.documentIds().forEach((documentId: DocumentId) => { - if (documentId.rid === updatedDocument._rid) { - const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( - updatedDocument, - this._getPartitionKeyDefinition() as PartitionKeyDefinition, - ); - - const id = new ObjectId(this, updatedDocument, partitionKeyArray); - documentId.id(id.id()); - } - }); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Update document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public buildQuery(filter: string): string { - return filter || "{}"; - } - - public async selectDocument(documentId: DocumentId): Promise { - this.selectedDocumentId(documentId); - const content = await readDocument(this.collection.databaseId, this.collection, documentId); - this.initDocumentEditor(documentId, content); - } - - public loadNextPage(): Q.Promise { - this.isExecuting(true); - this.isExecutionError(false); - const filter: string = this.filterContent().trim(); - const query: string = this.buildQuery(filter); - - return Q(queryDocuments(this.collection.databaseId, this.collection, true, query, this.continuationToken)) - .then( - ({ continuationToken, documents }) => { - this.continuationToken = continuationToken; - let currentDocuments = this.documentIds(); - const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); - const nextDocumentIds = documents - .filter((d: any) => { - return currentDocumentsRids.indexOf(d._rid) < 0; - }) - .map((rawDocument: any) => { - const partitionKeyValue = rawDocument._partitionKeyValue; - return new DocumentId(this, rawDocument, [partitionKeyValue]); - }); - - const merged = currentDocuments.concat(nextDocumentIds); - - this.documentIds(merged); - currentDocuments = this.documentIds(); - if (this.filterContent().length > 0 && currentDocuments.length > 0) { - currentDocuments[0].click(); - } else { - this.selectedDocumentContent(""); - this.selectedDocumentId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - } - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - (error: any) => { - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - ) - .finally(() => this.isExecuting(false)); - } - - protected _onEditorContentChange(newContent: string) { - try { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid - ) { - let parsed: any = JSON.parse(newContent); - } - - // Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit - this.onValidDocumentEdit(); - } catch (e) { - this.onInvalidDocumentEdit(); - } - } - - /** Renders a Javascript object to be displayed inside Monaco Editor */ - public renderObjectForEditor(value: any, replacer: any, space: string | number): string { - return MongoUtility.tojson(value, null, false); - } - - private _hasShardKeySpecified(document: any): boolean { - return Boolean(extractPartitionKeyValues(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition)); - } - - private _getPartitionKeyDefinition(): DataModels.PartitionKey { - let partitionKey: DataModels.PartitionKey = this.partitionKey; - - if ( - this.partitionKey && - this.partitionKey.paths && - this.partitionKey.paths.length && - this.partitionKey.paths.length > 0 && - this.partitionKey.paths[0].indexOf("$v") > -1 - ) { - // Convert BsonSchema2 to /path format - partitionKey = { - kind: partitionKey.kind, - paths: ["/" + this.partitionKeyProperties?.[0].replace(/\./g, "/")], - version: partitionKey.version, - }; - } - - return partitionKey; - } - - protected __deleteDocument(documentId: DocumentId): Promise { - return deleteDocument(this.collection.databaseId, this.collection, documentId); - } -} diff --git a/src/Explorer/Tabs/useTabs.test.ts b/src/Explorer/Tabs/useTabs.test.ts index 90a13f827..9d4925a2c 100644 --- a/src/Explorer/Tabs/useTabs.test.ts +++ b/src/Explorer/Tabs/useTabs.test.ts @@ -1,17 +1,17 @@ +import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; -import { useTabs } from "../../hooks/useTabs"; import { updateUserContext } from "../../UserContext"; +import { useTabs } from "../../hooks/useTabs"; import { container } from "../Controls/Settings/TestUtils"; import DocumentId from "../Tree/DocumentId"; -import DocumentsTab from "./DocumentsTab"; import { NewQueryTab } from "./QueryTab/QueryTab"; describe("useTabs tests", () => { let database: ViewModels.Database; let collection: ViewModels.Collection; let queryTab: NewQueryTab; - let documentsTab: DocumentsTab; + let documentsTab: DocumentsTabV2; beforeEach(() => { updateUserContext({ @@ -56,7 +56,7 @@ describe("useTabs tests", () => { }, ); - documentsTab = new DocumentsTab({ + documentsTab = new DocumentsTabV2({ partitionKey: undefined, documentIds: ko.observableArray(), tabKind: ViewModels.CollectionTabKind.Documents, diff --git a/src/Explorer/Tree/ObjectId.ts b/src/Explorer/Tree/ObjectId.ts index fc53a6a37..314a4cd7e 100644 --- a/src/Explorer/Tree/ObjectId.ts +++ b/src/Explorer/Tree/ObjectId.ts @@ -1,9 +1,8 @@ import * as ko from "knockout"; -import DocumentId from "./DocumentId"; -import DocumentsTab from "../Tabs/DocumentsTab"; +import DocumentId, { IDocumentIdContainer } from "./DocumentId"; export default class ObjectId extends DocumentId { - constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { + constructor(container: IDocumentIdContainer, data: any, partitionKeyValue: any) { super(container, data, partitionKeyValue); if (typeof data._id === "object") { this.id = ko.observable(data._id[Object.keys(data._id)[0]]);