From 455949664009484c0e5160f6caf66e504eaeb8a2 Mon Sep 17 00:00:00 2001 From: sunilyadav840 Date: Thu, 24 Jun 2021 17:57:31 +0530 Subject: [PATCH] add load, create, update and delete Document --- less/documentDB.less | 60 ++ less/forms.less | 3 - src/Explorer/Controls/Editor/EditorReact.tsx | 9 +- .../QueryContainerComponent.tsx | 2 +- src/Explorer/Tabs/DocumentTabUtils.tsx | 51 ++ src/Explorer/Tabs/DocumentsTab1.tsx | 169 ++-- src/Explorer/Tabs/DocumentsTabContent.tsx | 775 +++++++++++++++--- src/Explorer/Tabs/MongoDocumentsTab.ts | 2 +- 8 files changed, 865 insertions(+), 206 deletions(-) create mode 100644 src/Explorer/Tabs/DocumentTabUtils.tsx diff --git a/less/documentDB.less b/less/documentDB.less index 159b03cd7..231052024 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -3088,4 +3088,64 @@ settings-pane { .hiddenMain { display: none; height: 0px; +} +.react-editor { + height: 400px; +} +.documentTabSearchBar{ + width: 80%; + margin: 15px; +} +.documentTabFiltetButton{ + margin-top: 15px; +} +.filterSuggestions { + z-index: 1; + position: absolute; + top: 133px; + padding: 10px; + margin-left: 10px; + background: white; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + width: 20%; + height: auto; +} +.documentTabSuggestions { + padding: 5px; + cursor: pointer; +} +.documentTabNoFilterView { + margin: 15px; +} +.noFilterText { + margin-right: 10px; +} +.documentTabWatermark{ + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin-top: 10%; + margin-right: 12%; +} +.documentCreateText { + margin-top: 10px; +} +.documentLoadMore { + color: #0078D4; + font-size: 12px; + cursor: pointer; + margin-top: 10px; +} +.leftSplitter { + align-items: center; + text-align: center; + flex-direction: column; +} +.documentIdItem{ + cursor: pointer; +} +.splitterWrapper .splitter-layout .layout-pane.layout-pane-primary{ + max-height: 60%; } \ No newline at end of file diff --git a/less/forms.less b/less/forms.less index e453a82b7..71510b156 100644 --- a/less/forms.less +++ b/less/forms.less @@ -201,6 +201,3 @@ .migration:disabled { background-color: #ccc; } -.react-editor { - height: 400px; -} \ No newline at end of file diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 71273ed20..c4eaabb80 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -10,6 +10,7 @@ export interface EditorReactProps { onContentChanged?: (newContent: string) => void; // Called when text is changed lineNumbers?: monaco.editor.IEditorOptions["lineNumbers"]; theme?: string; // Monaco editor theme + editorKey?: string; } export class EditorReact extends React.Component { @@ -27,13 +28,19 @@ export class EditorReact extends React.Component { public shouldComponentUpdate(): boolean { // Prevents component re-rendering - return false; + return true; } public componentWillUnmount(): void { this.selectionListener && this.selectionListener.dispose(); } + public componentDidUpdate(prevProps: EditorReactProps): void { + if (prevProps.editorKey !== this.props.editorKey) { + this.createEditor(this.configureEditor.bind(this)); + } + } + public render(): JSX.Element { return
this.setRef(elt)} />; } diff --git a/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx index f24e25cb6..8602d5263 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import CloseIcon from "../../../../images/close-black.svg"; +import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; export interface QueryContainerComponentProps { initialQuery: string; diff --git a/src/Explorer/Tabs/DocumentTabUtils.tsx b/src/Explorer/Tabs/DocumentTabUtils.tsx new file mode 100644 index 000000000..8cf119395 --- /dev/null +++ b/src/Explorer/Tabs/DocumentTabUtils.tsx @@ -0,0 +1,51 @@ +import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; +import * as DataModels from "../../Contracts/DataModels"; +import DocumentId from "../Tree/DocumentId"; + +export function hasShardKeySpecified( + document: string, + partitionKey: DataModels.PartitionKey, + partitionKeyProperty: string +): boolean { + return Boolean( + extractPartitionKey( + document, + getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition + ) + ); +} + +export function getPartitionKeyDefinition( + partitionKey: DataModels.PartitionKey, + partitionKeyProperty: string +): DataModels.PartitionKey { + if ( + partitionKey && + partitionKey.paths && + partitionKey.paths.length && + partitionKey.paths.length > 0 && + partitionKey.paths[0].indexOf("$v") > -1 + ) { + // Convert BsonSchema2 to /path format + partitionKey = { + kind: partitionKey.kind, + paths: ["/" + partitionKeyProperty.replace(/\./g, "/")], + version: partitionKey.version, + }; + } + return partitionKey; +} + +export function formatDocumentContent(row: DocumentId): string { + const { partitionKeyProperty, partitionKeyValue, rid, self, stringPartitionKeyValue, ts } = row; + const documentContent = JSON.stringify({ + partitionKeyProperty: partitionKeyProperty || "", + partitionKeyValue: partitionKeyValue || "", + rid: rid || "", + self: self || "", + stringPartitionKeyValue: stringPartitionKeyValue || "", + ts: ts || "", + }); + const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}"); + return formattedDocumentContent; +} diff --git a/src/Explorer/Tabs/DocumentsTab1.tsx b/src/Explorer/Tabs/DocumentsTab1.tsx index 4de8424e1..6d6c6313b 100644 --- a/src/Explorer/Tabs/DocumentsTab1.tsx +++ b/src/Explorer/Tabs/DocumentsTab1.tsx @@ -2,11 +2,7 @@ import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryItera import * as ko from "knockout"; import Q from "q"; import React from "react"; -import 'react-splitter-layout/lib/index.css'; -import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; -import DiscardIcon from "../../../images/discard.svg"; -import NewDocumentIcon from "../../../images/NewDocument.svg"; -import SaveIcon from "../../../images/save-cosmos.svg"; +import "react-splitter-layout/lib/index.css"; import UploadIcon from "../../../images/Upload_16x16.svg"; import * as Constants from "../../Common/Constants"; import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants"; @@ -75,6 +71,7 @@ export default class DocumentsTab extends TabsBase { constructor(options: ViewModels.DocumentsTabOptions) { super(options); + this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB; this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; @@ -791,92 +788,92 @@ export default class DocumentsTab extends TabsBase { return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey); } - protected getTabsButtons(): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = []; - const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; - if (this.newDocumentButton.visible()) { - buttons.push({ - iconSrc: NewDocumentIcon, - iconAlt: label, - onCommandClick: this.onNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.newDocumentButton.enabled(), - }); - } + // protected getTabsButtons(): CommandButtonComponentProps[] { + // const buttons: CommandButtonComponentProps[] = []; + // const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; + // if (this.newDocumentButton.visible()) { + // buttons.push({ + // iconSrc: NewDocumentIcon, + // iconAlt: label, + // onCommandClick: this.onNewDocumentClick, + // commandButtonLabel: label, + // ariaLabel: label, + // hasPopup: false, + // disabled: !this.newDocumentButton.enabled(), + // }); + // } - if (this.saveNewDocumentButton.visible()) { - const label = "Save"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - onCommandClick: this.onSaveNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.saveNewDocumentButton.enabled(), - }); - } + // if (this.saveNewDocumentButton.visible()) { + // const label = "Save"; + // buttons.push({ + // iconSrc: SaveIcon, + // iconAlt: label, + // onCommandClick: this.onSaveNewDocumentClick, + // commandButtonLabel: label, + // ariaLabel: label, + // hasPopup: false, + // disabled: !this.saveNewDocumentButton.enabled(), + // }); + // } - if (this.discardNewDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - onCommandClick: this.onRevertNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.discardNewDocumentChangesButton.enabled(), - }); - } + // if (this.discardNewDocumentChangesButton.visible()) { + // const label = "Discard"; + // buttons.push({ + // iconSrc: DiscardIcon, + // iconAlt: label, + // onCommandClick: this.onRevertNewDocumentClick, + // commandButtonLabel: label, + // ariaLabel: label, + // hasPopup: false, + // disabled: !this.discardNewDocumentChangesButton.enabled(), + // }); + // } - if (this.saveExisitingDocumentButton.visible()) { - const label = "Update"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - onCommandClick: this.onSaveExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.saveExisitingDocumentButton.enabled(), - }); - } + // if (this.saveExisitingDocumentButton.visible()) { + // const label = "Update"; + // buttons.push({ + // iconSrc: SaveIcon, + // iconAlt: label, + // onCommandClick: this.onSaveExisitingDocumentClick, + // commandButtonLabel: label, + // ariaLabel: label, + // hasPopup: false, + // disabled: !this.saveExisitingDocumentButton.enabled(), + // }); + // } - if (this.discardExisitingDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - onCommandClick: this.onRevertExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.discardExisitingDocumentChangesButton.enabled(), - }); - } + // if (this.discardExisitingDocumentChangesButton.visible()) { + // const label = "Discard"; + // buttons.push({ + // iconSrc: DiscardIcon, + // iconAlt: label, + // onCommandClick: this.onRevertExisitingDocumentClick, + // commandButtonLabel: label, + // ariaLabel: label, + // hasPopup: false, + // disabled: !this.discardExisitingDocumentChangesButton.enabled(), + // }); + // } - if (this.deleteExisitingDocumentButton.visible()) { - const label = "Delete"; - buttons.push({ - iconSrc: DeleteDocumentIcon, - iconAlt: label, - onCommandClick: this.onDeleteExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.deleteExisitingDocumentButton.enabled(), - }); - } + // if (this.deleteExisitingDocumentButton.visible()) { + // const label = "Delete"; + // buttons.push({ + // iconSrc: DeleteDocumentIcon, + // iconAlt: label, + // onCommandClick: this.onDeleteExisitingDocumentClick, + // commandButtonLabel: label, + // ariaLabel: label, + // hasPopup: false, + // disabled: !this.deleteExisitingDocumentButton.enabled(), + // }); + // } - if (!this.isPreferredApiMongoDB) { - buttons.push(DocumentsTab._createUploadButton(this.collection.container)); - } + // if (!this.isPreferredApiMongoDB) { + // buttons.push(DocumentsTab._createUploadButton(this.collection.container)); + // } - return buttons; - } + // return buttons; + // } protected buildCommandBarOptions(): void { ko.computed(() => @@ -925,8 +922,6 @@ export default class DocumentsTab extends TabsBase { } render(): JSX.Element { - return ( - - ) + return ; } } diff --git a/src/Explorer/Tabs/DocumentsTabContent.tsx b/src/Explorer/Tabs/DocumentsTabContent.tsx index 7613a036b..567098a86 100644 --- a/src/Explorer/Tabs/DocumentsTabContent.tsx +++ b/src/Explorer/Tabs/DocumentsTabContent.tsx @@ -1,155 +1,704 @@ -import { DetailsList, DetailsListLayoutMode, IColumn, Selection, SelectionMode } from '@fluentui/react/lib/DetailsList'; -import * as React from 'react'; -import SplitterLayout from 'react-splitter-layout'; +import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; +import { + DetailsList, + DetailsListLayoutMode, + IColumn, + IIconProps, + IImageProps, + Image, + ImageFit, + List, + PrimaryButton, + SelectionMode, + Stack, + Text, + TextField, +} from "@fluentui/react"; +import {} from "@fluentui/react/lib/Image"; +import * as React from "react"; +import SplitterLayout from "react-splitter-layout"; +import CloseIcon from "../../../images/close-black.svg"; +import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; +import DiscardIcon from "../../../images/discard.svg"; +import DocumentWaterMark from "../../../images/DocumentWaterMark.svg"; +import NewDocumentIcon from "../../../images/NewDocument.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import { Areas } from "../../Common/Constants"; +// import { createDocument } from "../../Common/dataAccess/createDocument"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import { logError } from "../../Common/Logger"; +import { createDocument, deleteDocument, queryDocuments, updateDocument } from "../../Common/MongoProxyClient"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "../Controls/Editor/EditorReact"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import DocumentId from "../Tree/DocumentId"; +import ObjectId from "../Tree/ObjectId"; +import DocumentsTab from "./DocumentsTab"; +import { formatDocumentContent, getPartitionKeyDefinition, hasShardKeySpecified } from "./DocumentTabUtils"; +const filterIcon: IIconProps = { iconName: "Filter" }; -export interface IDetailsListDocumentsExampleState { +export interface IDocumentsTabContentState { columns: IColumn[]; - items: any; - selectionDetails: string; isModalSelection: boolean; isCompactMode: boolean; announcedMessage?: string; + isSuggestionVisible: boolean; + filter: string; + isFilterOptionVisible: boolean; + isEditorVisible: boolean; + documentContent: string; + documentIds: Array; + editorKey: string; + selectedDocumentId?: DocumentId; } export interface IDocument { - key: string; - name: string; value: string; - iconName: string; - fileType: string; - modifiedBy: string; - dateModified: string; - dateModifiedValue: number; - fileSize: string; - fileSizeRaw: number; + id: string; } -export default class DocumentsTabContent extends React.Component<{}, IDetailsListDocumentsExampleState> { - private _selection: Selection; +interface IButton { + visible: boolean; + enabled: boolean; + isSelected?: boolean; +} - constructor(props: {}) { +const imageProps: Partial = { + imageFit: ImageFit.centerContain, + width: 40, + height: 40, + style: { marginTop: "15px" }, +}; + +const filterSuggestions = [{ value: `{"id": "foo"}` }, { value: "{ qty: { $gte: 20 } }" }]; +const intitalDocumentContent = `{ \n "id": "replace_with_new_document_id" \n }`; +export default class DocumentsTabContent extends React.Component { + public newDocumentButton: IButton; + public saveNewDocumentButton: IButton; + public discardNewDocumentChangesButton: IButton; + public saveExisitingDocumentButton: IButton; + public discardExisitingDocumentChangesButton: IButton; + public deleteExisitingDocumentButton: IButton; + + constructor(props: DocumentsTab) { super(props); + this.newDocumentButton = { + visible: true, + enabled: true, + }; + this.saveNewDocumentButton = { + visible: false, + enabled: true, + }; + this.discardNewDocumentChangesButton = { + visible: false, + enabled: false, + }; + this.saveExisitingDocumentButton = { + visible: false, + enabled: false, + }; + this.discardExisitingDocumentChangesButton = { + visible: false, + enabled: false, + }; + this.deleteExisitingDocumentButton = { + visible: false, + enabled: false, + }; + const columns: IColumn[] = [ { - key: 'column4', - name: 'Modified By', - fieldName: 'modifiedBy', - minWidth: 70, - maxWidth: 90, + key: "_id", + name: props.idHeader, + minWidth: 90, + maxWidth: 140, isResizable: true, isCollapsible: true, - data: 'string', - onColumnClick: this._onColumnClick, - onRender: (item: IDocument) => { - return {item.modifiedBy}; + data: "string", + onRender: (item: DocumentId) => { + return ( +
this.handleRow(item)} className="documentIdItem"> + {item.rid} +
+ ); }, isPadded: true, }, { - key: 'column5', - name: 'File Size', - fieldName: 'fileSizeRaw', - minWidth: 70, - maxWidth: 90, + key: "column2", + name: props.partitionKeyPropertyHeader, + minWidth: 50, + maxWidth: 60, isResizable: true, isCollapsible: true, - data: 'number', - onColumnClick: this._onColumnClick, - onRender: (item: IDocument) => { - return {item.fileSize}; - }, + data: "number", }, ]; - this._selection = new Selection({ - onSelectionChanged: () => { - this.setState({ - selectionDetails: this._getSelectionDetails(), - }); - }, - }); - - const items = [ - { - fileSize: "44 KB", - modifiedBy: "Dolor Sit", - }, - { - fileSize: "44 KB", - modifiedBy: "Dolor Sit", - } - ] - this.state = { - items: items, columns: columns, - selectionDetails: this._getSelectionDetails(), isModalSelection: false, isCompactMode: false, announcedMessage: undefined, + isSuggestionVisible: false, + filter: "", + isFilterOptionVisible: true, + isEditorVisible: false, + documentContent: intitalDocumentContent, + documentIds: [], + editorKey: "", }; } - public render(): JSX.Element { - const { columns, isCompactMode, items } = this.state; - - return ( -
- - -
- -
-
-
- - ); - } - - private _getKey(item: any, index?: number): string { - return item.key; - } - - - private _onItemInvoked(item: any): void { - alert(`Item invoked: ${item.name}`); - } - - private _getSelectionDetails(): string { - const selectionCount = this._selection.getSelectedCount(); - - switch (selectionCount) { - case 0: - return 'No items selected'; - case 1: - return '1 item selected: ' + (this._selection.getSelection()[0] as IDocument).name; - default: - return `${selectionCount} items selected`; + async componentDidMount(): void { + this.updateTabButton(); + if (userContext.apiType === "Mongo") { + this.queryDocumentsData(); } } - private _onColumnClick = (ev: React.MouseEvent, column: IColumn): void => { - console.log("====>", column) + queryDocumentsData = async (): Promise => { + this.props.isExecuting(true); + this.props.isExecutionError(false); + try { + const { filter } = this.state; + const query: string = filter || "{}"; + const queryDocumentsData = await queryDocuments( + this.props.collection.databaseId, + this.props.collection, + true, + query, + undefined + ); + if (queryDocumentsData) { + const nextDocumentIds = queryDocumentsData.documents.map((rawDocument: any) => { + const partitionKeyValue = rawDocument._partitionKeyValue; + return new DocumentId(this.props, rawDocument, partitionKeyValue); + }); + + this.setState({ documentIds: nextDocumentIds }); + } + if (this.props.onLoadStartKey !== undefined) { + TelemetryProcessor.traceSuccess( + Action.Tab, + { + databaseName: this.props.collection.databaseId, + collectionName: this.props.collection.id(), + + dataExplorerArea: Areas.Tab, + tabTitle: this.props.tabTitle(), + }, + this.props.onLoadStartKey + ); + } + this.props.isExecuting(false); + } catch (error) { + if (this.props.onLoadStartKey !== undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseName: this.props.collection.databaseId, + collectionName: this.props.collection.id(), + + dataExplorerArea: Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + this.props.onLoadStartKey + ); + } + this.props.isExecuting(false); + } + }; + + handleRow = (row: DocumentId): void => { + const formattedDocumentContent = formatDocumentContent(row); + this.newDocumentButton = { + visible: false, + enabled: false, + }; + this.saveNewDocumentButton = { + visible: false, + enabled: false, + }; + this.discardNewDocumentChangesButton = { + visible: false, + enabled: false, + }; + this.saveExisitingDocumentButton = { + visible: true, + enabled: false, + }; + this.discardExisitingDocumentChangesButton = { + visible: true, + enabled: false, + }; + this.deleteExisitingDocumentButton = { + visible: true, + enabled: true, + }; + this.setState( + { + documentContent: formattedDocumentContent, + isEditorVisible: true, + editorKey: row.rid, + selectedDocumentId: row, + }, + () => { + this.updateTabButton(); + } + ); + }; + + formatDocumentContent = (row: DocumentId): string => { + const { partitionKeyProperty, partitionKeyValue, rid, self, stringPartitionKeyValue, ts } = row; + const documentContent = JSON.stringify({ + partitionKeyProperty: partitionKeyProperty || "", + partitionKeyValue: partitionKeyValue || "", + rid: rid || "", + self: self || "", + stringPartitionKeyValue: stringPartitionKeyValue || "", + ts: ts || "", + }); + const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}"); + return formattedDocumentContent; + }; + + handleFilter = (): void => { + this.queryDocumentsData(); + this.setState({ + isSuggestionVisible: false, + }); + }; + + private async updateMongoDocument(): Promise { + const { selectedDocumentId, documentContent, documentIds } = this.state; + const { isExecutionError, isExecuting, tabTitle, collection, partitionKey, partitionKeyProperty } = this.props; + isExecutionError(false); + isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }); + + try { + const updatedDocument = await updateDocument( + collection.databaseId, + collection, + selectedDocumentId, + documentContent + ); + documentIds.forEach((documentId: DocumentId) => { + if (documentId.rid === updatedDocument._rid) { + const partitionKeyArray = extractPartitionKey( + updatedDocument, + getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition + ); + + const partitionKeyValue = partitionKeyArray && partitionKeyArray[0]; + + const id = new ObjectId(this.props, updatedDocument, partitionKeyValue); + documentId.id(id.id()); + } + }); + TelemetryProcessor.traceSuccess( + Action.UpdateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }, + startKey + ); + isExecuting(false); + } catch (error) { + isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.UpdateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + isExecuting(false); + } + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + const label = "New Document"; + if (this.newDocumentButton.visible) { + buttons.push({ + iconSrc: NewDocumentIcon, + iconAlt: label, + onCommandClick: this.onNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.newDocumentButton.enabled, + }); + } + + if (this.saveNewDocumentButton.visible) { + const label = "Save"; + buttons.push({ + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveNewDocumentButton.enabled, + }); + } + + if (this.discardNewDocumentChangesButton.visible) { + const label = "Discard"; + buttons.push({ + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onRevertNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardNewDocumentChangesButton.enabled, + }); + } + + if (this.saveExisitingDocumentButton.visible) { + const label = "Update"; + buttons.push({ + ...this, + updateMongoDocument: this.updateMongoDocument, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveExisitingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveExisitingDocumentButton.enabled, + }); + } + + if (this.discardExisitingDocumentChangesButton.visible) { + const label = "Discard"; + buttons.push({ + ...this, + setState: this.setState, + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onRevertExisitingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardExisitingDocumentChangesButton.enabled, + }); + } + + if (this.deleteExisitingDocumentButton.visible) { + const label = "Delete"; + buttons.push({ + ...this, + iconSrc: DeleteDocumentIcon, + iconAlt: label, + onCommandClick: this.onDeleteExisitingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.deleteExisitingDocumentButton.enabled, + }); + } + + return buttons; + } + + private onSaveExisitingDocumentClick(): void { + this.updateMongoDocument(); + } + + private onDeleteExisitingDocumentClick(): Promise { + const { collection } = this.props; + return deleteDocument(collection.databaseId, collection, this.state.selectedDocumentId); + } + + private onRevertExisitingDocumentClick(): void { + this.setState({ + documentContent: formatDocumentContent(this.state.selectedDocumentId), + editorKey: Math.random().toString(), + }); + } + + private onNewDocumentClick = () => { + this.newDocumentButton = { + visible: true, + enabled: false, + }; + this.saveNewDocumentButton = { + visible: true, + enabled: true, + }; + + this.discardNewDocumentChangesButton = { + visible: true, + enabled: true, + }; + this.updateTabButton(); + this.setState({ + documentContent: intitalDocumentContent, + isEditorVisible: true, + editorKey: intitalDocumentContent, + }); + }; + + private onSaveNewDocumentClick = () => { + if (userContext.apiType === "Mongo") { + this.onSaveNewMongoDocumentClick(); + } + }; + + public onSaveNewMongoDocumentClick = async (): Promise => { + const parsedDocumentContent = JSON.parse(this.state.documentContent); + const { + partitionKey, + partitionKeyProperty, + displayedError, + tabTitle, + isExecutionError, + isExecuting, + collection, + } = this.props; + displayedError(""); + + const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }); + + if ( + partitionKeyProperty && + partitionKeyProperty !== "_id" && + !hasShardKeySpecified(parsedDocumentContent, partitionKey, partitionKeyProperty) + ) { + const message = `The document is lacking the shard property: ${partitionKeyProperty}`; + displayedError(message); + TelemetryProcessor.traceFailure( + Action.CreateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + error: message, + }, + startKey + ); + logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); + throw new Error("Document without shard key"); + } + + isExecutionError(false); + isExecuting(true); + try { + const savedDocument = await createDocument( + collection.databaseId, + collection, + partitionKeyProperty, + parsedDocumentContent + ); + if (savedDocument) { + this.handleLoadMoreDocument(); + TelemetryProcessor.traceSuccess( + Action.CreateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }, + startKey + ); + } + this.queryDocumentsData(); + isExecuting(false); + } catch (error) { + isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + isExecuting(false); + } + }; + + private onRevertNewDocumentClick = () => { + this.newDocumentButton = { + visible: true, + enabled: true, + }; + this.saveNewDocumentButton = { + visible: true, + enabled: false, + }; + + this.discardNewDocumentChangesButton = { + visible: true, + enabled: false, + }; + + this.updateTabButton(); + this.setState({ isEditorVisible: false }); + }; + + onRenderCell = (item: { value: string }): JSX.Element => { + return ( +
+ this.setState({ + filter: item.value, + isSuggestionVisible: false, + }) + } + > + {item.value} +
+ ); + }; + + handleLoadMoreDocument = (): void => { + this.queryDocumentsData(); + this.setState({ isSuggestionVisible: false }); + }; + + private updateTabButton = (): void => { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }; + + private handleDocumentContentChange = (newContent: string): void => { + if (this.saveExisitingDocumentButton.visible) { + this.saveExisitingDocumentButton = { + visible: true, + enabled: true, + }; + this.discardExisitingDocumentChangesButton = { + visible: true, + enabled: true, + }; + + this.updateTabButton(); + } + + this.setState({ documentContent: newContent }); + }; + + public render(): JSX.Element { + const { + columns, + isCompactMode, + isSuggestionVisible, + filter, + isFilterOptionVisible, + isEditorVisible, + documentContent, + documentIds, + editorKey, + } = this.state; + + return ( +
+ {isFilterOptionVisible && ( +
+
+ + this.setState({ isSuggestionVisible: true })} + onChange={(_event, newInput?: string) => { + this.setState({ filter: newInput }); + }} + value={filter} + /> + + Close icon this.setState({ isFilterOptionVisible: false })} + /> + +
+ {isSuggestionVisible && ( +
+ +
+ )} +
+ )} + {!isFilterOptionVisible && ( + + No filter applied + this.setState({ isFilterOptionVisible: true })} /> + + )} +
this.setState({ isSuggestionVisible: false })}> + +
+ + + Load More + +
+ {isEditorVisible ? ( +
+ +
+ ) : ( +
+ Document watermark + Create new or work with existing document(s). +
+ )} +
+
+
+ ); + } + + private getKey(item: DocumentId): string { + return item.rid; } } diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index 477173d4b..bacf88750 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -9,7 +9,7 @@ import { deleteDocument, queryDocuments, readDocument, - updateDocument + updateDocument, } from "../../Common/MongoProxyClient"; import MongoUtility from "../../Common/MongoUtility"; import * as DataModels from "../../Contracts/DataModels";