import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; import { DetailsList, DetailsListLayoutMode, IColumn, IIconProps, IImageProps, Image, ImageFit, List, PrimaryButton, SelectionMode, Stack, Text, TextField } from "@fluentui/react"; 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 * as ViewModels from "../../Contracts/ViewModels"; 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 "./DocumentsTab1"; import { formatDocumentContent, getPartitionKeyDefinition, hasShardKeySpecified } from "./DocumentTabUtils"; const filterIcon: IIconProps = { iconName: "Filter" }; export interface IDocumentsTabContentState { columns: IColumn[]; isModalSelection: boolean; isCompactMode: boolean; announcedMessage?: string; isSuggestionVisible: boolean; filter: string; isFilterOptionVisible: boolean; isEditorVisible: boolean; documentContent: string; documentIds: Array; editorKey: string; selectedDocumentId?: DocumentId; isEditorContentEdited: boolean; } export interface IDocument { value: string; id: string; } interface IButton { visible: boolean; enabled: boolean; isSelected?: boolean; } 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 }`; const idHeader = userContext.apiType === "Mongo" ? "_id" : "id"; 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: "_id", name: idHeader, minWidth: 90, maxWidth: 140, isResizable: true, isCollapsible: true, data: "string", onRender: (item: DocumentId) => { return (
this.handleRow(item)} className="documentIdItem"> {item.rid}
); }, isPadded: true, }, { key: "column2", name: props.partitionKeyPropertyHeader, minWidth: 50, maxWidth: 60, isResizable: true, isCollapsible: true, data: "number", }, ]; this.state = { columns: columns, isModalSelection: false, isCompactMode: false, announcedMessage: undefined, isSuggestionVisible: false, filter: "", isFilterOptionVisible: true, isEditorVisible: false, documentContent: intitalDocumentContent, documentIds: [], editorKey: "", isEditorContentEdited: false, }; } componentDidMount(): void { this.props.isExecuting(true); this.updateTabButton(); if (userContext.apiType === "Mongo") { this.queryDocumentsData(); } } 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 as ViewModels.Collection, true, query, undefined ); if (queryDocumentsData) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const nextDocumentIds = queryDocumentsData.documents.map((rawDocument: any) => { const partitionKeyValue = rawDocument.partitionKeyValue; return new DocumentId(this.props as DocumentsTab, 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 => { if (this.state.isEditorContentEdited) { const isChangesConfirmed = window.confirm("Your unsaved changes will be lost.") if (isChangesConfirmed) { this.handleRowContent(row); this.setState({ isEditorContentEdited: false }); return; } } else { this.handleRowContent(row); } }; handleRowContent = (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 as ViewModels.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 as DocumentsTab, updatedDocument, partitionKeyValue); documentId.id(id.id()); } }); TelemetryProcessor.traceSuccess( Action.UpdateDocument, { dataExplorerArea: Areas.Tab, tabTitle: tabTitle(), }, startKey ); this.setState({ isEditorContentEdited: false }); 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 async onDeleteExisitingDocumentClick(): Promise { const msg = userContext.apiType !== "Mongo" ? "Are you sure you want to delete the selected item ?" : "Are you sure you want to delete the selected document ?"; const { isExecutionError, isExecuting, collection, } = this.props; if (window.confirm(msg)) { try { isExecuting(true) await deleteDocument(collection.databaseId, collection as ViewModels.Collection, this.state.selectedDocumentId); isExecuting(false) } catch (error) { console.error(error); isExecutionError(true); isExecuting(false) } } } 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 as ViewModels.Collection, partitionKeyProperty, parsedDocumentContent ); if (savedDocument) { this.handleLoadMoreDocument(); TelemetryProcessor.traceSuccess( Action.CreateDocument, { dataExplorerArea: Areas.Tab, tabTitle: tabTitle(), }, startKey ); } this.setState({ isEditorContentEdited: false }); 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, isEditorContentEdited: 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, isEditorContentEdited: true, }); }; 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; } }