diff --git a/less/documentDB.less b/less/documentDB.less index 231052024..3188c7899 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -3096,6 +3096,10 @@ settings-pane { width: 80%; margin: 15px; } +.documentSqlTabSearchBar{ + width: 68%; + margin: 15px; +} .documentTabFiltetButton{ margin-top: 15px; } @@ -3111,6 +3115,9 @@ settings-pane { width: 20%; height: auto; } +.sqlFilterSuggestions { + margin-left: 10%; +} .documentTabSuggestions { padding: 5px; cursor: pointer; @@ -3148,4 +3155,9 @@ settings-pane { } .splitterWrapper .splitter-layout .layout-pane.layout-pane-primary{ max-height: 60%; +} +.queryText { + padding: 20px; + padding-left: 10px; + padding-right: 0px; } \ No newline at end of file diff --git a/src/Common/dataAccess/deleteDocument.ts b/src/Common/dataAccess/deleteDocument.ts index 0ab2e6999..9e9f432ee 100644 --- a/src/Common/dataAccess/deleteDocument.ts +++ b/src/Common/dataAccess/deleteDocument.ts @@ -1,12 +1,13 @@ import { CollectionBase } from "../../Contracts/ViewModels"; +import DocumentId from "../../Explorer/Tree/DocumentId"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { getEntityName } from "../DocumentUtility"; import { handleError } from "../ErrorHandlingUtils"; -import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import DocumentId from "../../Explorer/Tree/DocumentId"; export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise => { const entityName: string = getEntityName(); + console.log("documemt", documentId); const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`); try { diff --git a/src/Explorer/Tabs/DocumentTabUtils.tsx b/src/Explorer/Tabs/DocumentTabUtils.tsx index 8cf119395..e5cdefa9c 100644 --- a/src/Explorer/Tabs/DocumentTabUtils.tsx +++ b/src/Explorer/Tabs/DocumentTabUtils.tsx @@ -1,4 +1,5 @@ import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; +import { Resource } from "../../../src/Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels"; import DocumentId from "../Tree/DocumentId"; @@ -49,3 +50,48 @@ export function formatDocumentContent(row: DocumentId): string { const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}"); return formattedDocumentContent; } + +export function formatSqlDocumentContent(row: Resource): string { + const { id, _rid, _self, _ts, _etag } = row; + const documentContent = JSON.stringify({ + id: id || "", + _rid: _rid || "", + _self: _self || "", + _ts: _ts || "", + _etag: _etag || "", + }); + const formattedDocumentContent = documentContent.replace(/,/g, ",\n").replace("{", "{\n").replace("}", "\n}"); + return formattedDocumentContent; +} + +export function getFilterPlaceholder(isPreferredApiMongoDB: boolean): string { + const filterPlaceholder = isPreferredApiMongoDB + ? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents." + : "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."; + return filterPlaceholder; +} + +export function getFilterSuggestions(isPreferredApiMongoDB: boolean): { value: string }[] { + const filterSuggestions = isPreferredApiMongoDB + ? [{ value: `{"id": "foo"}` }, { value: "{ qty: { $gte: 20 } }" }] + : [ + { value: 'WHERE c.id = "foo"' }, + { value: "ORDER BY c._ts DESC" }, + { value: 'WHERE c.id = "foo" ORDER BY c._ts DESC' }, + ]; + return filterSuggestions; +} + +export function getDocumentItems( + isPreferredApiMongoDB: boolean, + documentIds: Array, + documentSqlIds: Array, + isAllDocumentsVisible: boolean +): Array | Array { + if (isPreferredApiMongoDB) { + const documentItems = documentIds.reverse(); + return isAllDocumentsVisible ? documentItems : documentItems.slice(0, 5); + } + const documentSqlItems = documentSqlIds.reverse(); + return isAllDocumentsVisible ? documentSqlItems : documentSqlItems.slice(0, 5); +} diff --git a/src/Explorer/Tabs/DocumentsTab1.tsx b/src/Explorer/Tabs/DocumentsTab1.tsx index dd52ce418..64dfcfde8 100644 --- a/src/Explorer/Tabs/DocumentsTab1.tsx +++ b/src/Explorer/Tabs/DocumentsTab1.tsx @@ -46,11 +46,10 @@ export default class DocumentsTab extends TabsBase { // public documentIds: ko.ObservableArray; // private _documentsIterator: QueryIterator; - // private _resourceTokenPartitionKey: string; + public _resourceTokenPartitionKey: string; constructor(options: ViewModels.DocumentsTabOptions) { super(options); - // this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB; // this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; @@ -64,7 +63,7 @@ export default class DocumentsTab extends TabsBase { // this.selectedDocumentContent = editable.observable(""); // this.initialDocumentContent = ko.observable(""); this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey); - // this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; + this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; // this.documentIds = options.documentIds; this.partitionKeyPropertyHeader = diff --git a/src/Explorer/Tabs/DocumentsTabContent.tsx b/src/Explorer/Tabs/DocumentsTabContent.tsx index a30d5999f..f00511b90 100644 --- a/src/Explorer/Tabs/DocumentsTabContent.tsx +++ b/src/Explorer/Tabs/DocumentsTabContent.tsx @@ -22,22 +22,37 @@ 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 UploadIcon from "../../../images/Upload_16x16.svg"; +import { Resource } from "../../../src/Contracts/DataModels"; import { Areas } from "../../Common/Constants"; -// import { createDocument } from "../../Common/dataAccess/createDocument"; +import { createDocument as createSqlDocuments } from "../../Common/dataAccess/createDocument"; +import { deleteDocument as deleteSqlDocument } from "../../Common/dataAccess/deleteDocument"; +import { queryDocuments as querySqlDocuments } from "../../Common/dataAccess/queryDocuments"; +import { updateDocument as updateSqlDocuments } from "../../Common/dataAccess/updateDocument"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import * as HeadersUtility from "../../Common/HeadersUtility"; 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 * as QueryUtils from "../../Utils/QueryUtils"; 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"; +import { + formatDocumentContent, + formatSqlDocumentContent, + getDocumentItems, + getFilterPlaceholder, + getFilterSuggestions, + getPartitionKeyDefinition, + hasShardKeySpecified +} from "./DocumentTabUtils"; const filterIcon: IIconProps = { iconName: "Filter" }; @@ -52,9 +67,16 @@ export interface IDocumentsTabContentState { isEditorVisible: boolean; documentContent: string; documentIds: Array; + documentSqlIds: Array; editorKey: string; selectedDocumentId?: DocumentId; + selectedSqlDocumentId?: Resource; isEditorContentEdited: boolean; + isAllDocumentsVisible: boolean; +} + +export interface IDocumentsTabContentProps extends DocumentsTab { + _resourceTokenPartitionKey: string; } export interface IDocument { @@ -75,9 +97,7 @@ const imageProps: Partial = { 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; @@ -118,7 +138,7 @@ export default class DocumentsTabContent extends React.Component { return (
this.handleRow(item)} className="documentIdItem"> - {item.rid} + {userContext.apiType === "Mongo" ? item.rid : item.id}
); }, @@ -155,8 +175,10 @@ export default class DocumentsTabContent extends React.Component => { + this.props.isExecuting(true); + this.props.isExecutionError(false); + const { filter } = this.state; + const query: string = this.buildQuery(filter); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + if (this.props._resourceTokenPartitionKey) { + options.partitionKey = this.props._resourceTokenPartitionKey; + } + + try { + const sqlQuery = querySqlDocuments(this.props.collection.databaseId, this.props.collection.id(), query, options); + const querySqlDocumentsData = await sqlQuery.fetchNext(); + this.setState({ documentSqlIds: querySqlDocumentsData.resources.length ? querySqlDocumentsData.resources : [] }); + this.props.isExecuting(false); + } catch (error) { + this.props.isExecuting(false); + this.props.isExecutionError(true); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + } + }; + queryDocumentsData = async (): Promise => { this.props.isExecuting(true); this.props.isExecutionError(false); @@ -223,9 +276,9 @@ export default class DocumentsTabContent extends React.Component { + handleRow = (row: DocumentId | Resource): void => { if (this.state.isEditorContentEdited) { - const isChangesConfirmed = window.confirm("Your unsaved changes will be lost.") + const isChangesConfirmed = window.confirm("Your unsaved changes will be lost."); if (isChangesConfirmed) { this.handleRowContent(row); this.setState({ isEditorContentEdited: false }); @@ -236,11 +289,14 @@ export default class DocumentsTabContent extends React.Component { - const formattedDocumentContent = formatDocumentContent(row); + handleRowContent = (row: DocumentId | Resource): void => { + const formattedDocumentContent = + userContext.apiType === "Mongo" + ? formatDocumentContent(row as DocumentId) + : formatSqlDocumentContent(row as Resource); this.newDocumentButton = { - visible: false, - enabled: false, + visible: true, + enabled: true, }; this.saveNewDocumentButton = { visible: false, @@ -262,6 +318,12 @@ export default class DocumentsTabContent extends React.Component { this.setState( { documentContent: formattedDocumentContent, @@ -273,29 +335,86 @@ export default class DocumentsTabContent extends React.Component { - 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; + updateSqlContent = (row: Resource, formattedDocumentContent: string): void => { + this.setState( + { + documentContent: formattedDocumentContent, + isEditorVisible: true, + editorKey: row._rid, + selectedSqlDocumentId: row, + }, + () => { + this.updateTabButton(); + } + ); }; handleFilter = (): void => { - this.queryDocumentsData(); + userContext.apiType === "Mongo" ? this.queryDocumentsData() : this.querySqlDocumentsData(); this.setState({ isSuggestionVisible: false, }); }; + async updateSqlDocument(): Promise { + const { partitionKey, partitionKeyProperty, isExecutionError, isExecuting, tabTitle, collection } = this.props; + const { documentContent } = this.state; + const partitionKeyArray = extractPartitionKey( + this.state.selectedSqlDocumentId, + getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition + ); + + const partitionKeyValue = partitionKeyArray && partitionKeyArray[0]; + const selectedDocumentId = new DocumentId( + this.props as DocumentsTab, + this.state.selectedSqlDocumentId, + partitionKeyValue + ); + isExecutionError(false); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }); + + try { + isExecuting(true); + const updateSqlDocumentRes = await updateSqlDocuments( + collection as ViewModels.Collection, + selectedDocumentId, + documentContent + ); + if (updateSqlDocumentRes) { + TelemetryProcessor.traceSuccess( + Action.UpdateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }, + startKey + ); + this.querySqlDocumentsData(); + isExecuting(false); + } + } catch (error) { + isExecutionError(true); + isExecuting(false); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); + TelemetryProcessor.traceFailure( + Action.UpdateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey + ); + } + } + private async updateMongoDocument(): Promise { const { selectedDocumentId, documentContent, documentIds } = this.state; const { isExecutionError, isExecuting, tabTitle, collection, partitionKey, partitionKeyProperty } = this.props; @@ -319,9 +438,7 @@ export default class DocumentsTabContent extends React.Component { + const selectedCollection: ViewModels.Collection = collection.container.findSelectedCollection(); + selectedCollection && collection.container.openUploadItemsPanePane(); + }, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: collection.container.isDatabaseNodeOrNoneSelected(), + }); + } return buttons; } private onSaveExisitingDocumentClick(): void { - this.updateMongoDocument(); + userContext.apiType === "Mongo" ? this.updateMongoDocument() : this.updateSqlDocument(); } 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 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; + const { isExecutionError, isExecuting, collection, tabTitle, partitionKey, partitionKeyProperty } = this.props; + const partitionKeyArray = extractPartitionKey( + this.state.selectedSqlDocumentId, + getPartitionKeyDefinition(partitionKey, partitionKeyProperty) as PartitionKeyDefinition + ); + + const partitionKeyValue = partitionKeyArray && partitionKeyArray[0]; + const selectedDocumentId = new DocumentId( + this.props as DocumentsTab, + this.state.selectedSqlDocumentId, + partitionKeyValue + ); + + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }); if (window.confirm(msg)) { try { - isExecuting(true) - await deleteDocument(collection.databaseId, collection as ViewModels.Collection, this.state.selectedDocumentId); - isExecuting(false) + isExecuting(true); + if (userContext.apiType === "Mongo") { + await deleteDocument( + collection.databaseId, + collection as ViewModels.Collection, + this.state.selectedDocumentId + ); + } else { + await deleteSqlDocument(collection as ViewModels.Collection, selectedDocumentId); + } + TelemetryProcessor.traceSuccess( + Action.DeleteDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }, + startKey + ); + this.setState({ isEditorVisible: false }); + isExecuting(false); + this.querySqlDocumentsData(); } catch (error) { - console.error(error); isExecutionError(true); - isExecuting(false) + isExecuting(false); + TelemetryProcessor.traceFailure( + Action.DeleteDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); } } } @@ -491,6 +666,19 @@ export default class DocumentsTabContent extends React.Component { if (userContext.apiType === "Mongo") { this.onSaveNewMongoDocumentClick(); + } else { + this.onSaveSqlNewMongoDocumentClick(); + } + }; + + public onSaveSqlNewMongoDocumentClick = async (): Promise => { + const { isExecutionError, tabTitle, isExecuting, collection } = this.props; + isExecutionError(false); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }); + const document = JSON.parse(this.state.documentContent); + + isExecuting(true); + + try { + const savedDocument = await createSqlDocuments(collection, document); + if (savedDocument) { + this.handleRowContent(savedDocument as Resource); + TelemetryProcessor.traceSuccess( + Action.CreateDocument, + { + dataExplorerArea: Areas.Tab, + tabTitle: tabTitle(), + }, + startKey + ); + } + isExecuting(false); + this.querySqlDocumentsData(); + } 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); } }; @@ -604,7 +838,6 @@ export default class DocumentsTabContent extends React.Component { @@ -624,8 +857,11 @@ export default class DocumentsTabContent extends React.Component { - this.queryDocumentsData(); - this.setState({ isSuggestionVisible: false }); + userContext.apiType === "Mongo" ? this.queryDocumentsData() : this.querySqlDocumentsData(); + this.setState({ + isSuggestionVisible: false, + isAllDocumentsVisible: true, + }); }; private updateTabButton = (): void => { @@ -642,14 +878,17 @@ export default class DocumentsTabContent extends React.Component { + this.updateTabButton(); + } + ); }; public render(): JSX.Element { @@ -662,19 +901,23 @@ export default class DocumentsTabContent extends React.Component {isFilterOptionVisible && (
+ {!isPreferredApiMongoDB && SELECT * FROM c} this.setState({ isSuggestionVisible: true })} onChange={(_event, newInput?: string) => { this.setState({ filter: newInput }); @@ -691,15 +934,15 @@ export default class DocumentsTabContent extends React.Component
{isSuggestionVisible && ( -
- +
+
)}
)} {!isFilterOptionVisible && ( - No filter applied + {isPreferredApiMongoDB ? "No filter applied" : "Select * from C"} this.setState({ isFilterOptionVisible: true })} /> )} @@ -707,7 +950,7 @@ export default class DocumentsTabContent extends React.Component