This commit is contained in:
Senthamil Sindhu 2024-06-27 11:00:31 -07:00
commit 912688dc14
15 changed files with 77 additions and 45 deletions

View File

@ -133,6 +133,7 @@ export enum MongoBackendEndpointType {
export class BackendApi { export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken"; public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings"; public static readonly PortalSettings: string = "PortalSettings";
public static readonly AccountRestrictions: string = "AccountRestrictions";
} }
export class PortalBackendEndpoints { export class PortalBackendEndpoints {

View File

@ -1,6 +1,7 @@
import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Logger from "../Common/Logger";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { getDataExplorerWindow } from "../Utils/WindowUtils"; import { getDataExplorerWindow } from "../Utils/WindowUtils";
import * as Constants from "./Constants"; import * as Constants from "./Constants";
@ -96,10 +97,18 @@ const _sendMessage = (message: any): void => {
const portalChildWindow = getDataExplorerWindow(window) || window; const portalChildWindow = getDataExplorerWindow(window) || window;
if (portalChildWindow === window) { if (portalChildWindow === window) {
// Current window is a child of portal, send message to portal window // Current window is a child of portal, send message to portal window
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*"); if (portalChildWindow.document.referrer) {
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer);
} else {
Logger.logError("Iframe failed to send message to portal", "MessageHandler");
}
} else { } else {
// Current window is not a child of portal, send message to the child window instead (which is data explorer) // Current window is not a child of portal, send message to the child window instead (which is data explorer)
portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*"); if (portalChildWindow.location.origin) {
portalChildWindow.postMessage(message, portalChildWindow.location.origin);
} else {
Logger.logError("Iframe failed to send message to data explorer", "MessageHandler");
}
} }
} }
}; };

View File

@ -2,7 +2,6 @@ import { CosmosClient } from "@azure/cosmos";
import { sampleDataClient } from "Common/SampleDataClient"; import { sampleDataClient } from "Common/SampleDataClient";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
@ -31,7 +30,6 @@ export async function readCollectionInternal(
collectionId: string, collectionId: string,
): Promise<DataModels.Collection> { ): Promise<DataModels.Collection> {
let collection: DataModels.Collection; let collection: DataModels.Collection;
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
try { try {
const response = await cosmosClient.database(databaseId).container(collectionId).read(); const response = await cosmosClient.database(databaseId).container(collectionId).read();
collection = response.resource as DataModels.Collection; collection = response.resource as DataModels.Collection;
@ -39,6 +37,5 @@ export async function readCollectionInternal(
handleError(error, "ReadCollection", `Error while querying container ${collectionId}`); handleError(error, "ReadCollection", `Error while querying container ${collectionId}`);
throw error; throw error;
} }
clearMessage();
return collection; return collection;
} }

View File

@ -162,7 +162,7 @@ export const addRootChildToGraph = (
* @param value * @param value
*/ */
export const escapeDoubleQuotes = (value: string): string => { export const escapeDoubleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/"/g, '\\"'); return value === undefined ? value : value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}; };
/** /**
@ -186,5 +186,5 @@ export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string =>
* @param value * @param value
*/ */
export const escapeSingleQuotes = (value: string): string => { export const escapeSingleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/'/g, "\\'"); return value === undefined ? value : value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}; };

View File

@ -576,9 +576,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</TooltipHost> </TooltipHost>
</Stack> </Stack>
<Text variant="small" aria-label="pkDescription"> <Text variant="small">{this.getPartitionKeySubtext()}</Text>
{this.getPartitionKeySubtext()}
</Text>
<input <input
type="text" type="text"

View File

@ -264,7 +264,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</TooltipHost> </TooltipHost>
</Stack> </Stack>
<Text variant="small" aria-label="pkDescription"> <Text variant="small">
{getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)} {getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)}
</Text> </Text>

View File

@ -223,7 +223,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</StyledTooltipHostBase> </StyledTooltipHostBase>
</Stack> </Stack>
<Text <Text
aria-label="pkDescription"
variant="small" variant="small"
/> />
<input <input

View File

@ -363,18 +363,24 @@ export class CassandraAPIDataClient extends TableDataClient {
entitiesToDelete: Entities.ITableEntity[], entitiesToDelete: Entities.ITableEntity[],
): Promise<any> { ): Promise<any> {
const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `;
const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); const partitionKeys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys;
await Promise.all( await Promise.all(
entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => { entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => {
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`); const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`);
const partitionKeyValue = currEntityToDelete[partitionKeyProperty];
const currQuery =
query +
(this.isStringType(partitionKeyValue.$)
? `${partitionKeyProperty} = '${partitionKeyValue._}'`
: `${partitionKeyProperty} = ${partitionKeyValue._}`);
let currQuery = query;
for (let partitionKeyIndex = 0; partitionKeyIndex < partitionKeys.length; partitionKeyIndex++) {
const partitionKey: CassandraTableKey = partitionKeys[partitionKeyIndex];
const partitionKeyValue: Entities.ITableEntityAttribute = currEntityToDelete[partitionKey.property];
currQuery =
currQuery +
(this.isStringType(partitionKeyValue.$)
? `${partitionKey.property} = '${partitionKeyValue._}'`
: `${partitionKey.property} = ${partitionKeyValue._}`);
if (partitionKeyIndex < partitionKeys.length - 1) {
currQuery = `${currQuery} AND `;
}
}
try { try {
await this.queryDocuments(collection, currQuery); await this.queryDocuments(collection, currQuery);
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`); NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`);

View File

@ -340,7 +340,7 @@ describe("Documents tab (noSql API)", () => {
isPreferredApiMongoDB: false, isPreferredApiMongoDB: false,
documentIds: [], documentIds: [],
collection: undefined, collection: undefined,
partitionKey: undefined, partitionKey: { kind: "Hash", paths: ["/foo"], version: 2 },
onLoadStartKey: 0, onLoadStartKey: 0,
tabTitle: "", tabTitle: "",
onExecutionErrorChange: (isExecutionError: boolean): void => { onExecutionErrorChange: (isExecutionError: boolean): void => {

View File

@ -7,7 +7,10 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import MongoUtility from "Common/MongoUtility"; import MongoUtility from "Common/MongoUtility";
import { StyleConstants } from "Common/StyleConstants"; import { StyleConstants } from "Common/StyleConstants";
import { createDocument } from "Common/dataAccess/createDocument"; import { createDocument } from "Common/dataAccess/createDocument";
import { deleteDocuments as deleteNoSqlDocuments } from "Common/dataAccess/deleteDocument"; import {
deleteDocument as deleteNoSqlDocument,
deleteDocuments as deleteNoSqlDocuments,
} from "Common/dataAccess/deleteDocument";
import { queryDocuments } from "Common/dataAccess/queryDocuments"; import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { readDocument } from "Common/dataAccess/readDocument"; import { readDocument } from "Common/dataAccess/readDocument";
import { updateDocument } from "Common/dataAccess/updateDocument"; import { updateDocument } from "Common/dataAccess/updateDocument";
@ -824,7 +827,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]); }, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]);
/** /**
* Implementation using bulk delete * Implementation using bulk delete NoSQL API
*/ */
let _deleteDocuments = useCallback( let _deleteDocuments = useCallback(
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => { async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
@ -834,7 +837,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
tabTitle, tabTitle,
}); });
setIsExecuting(true); setIsExecuting(true);
return deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
return (
partitionKey.systemKey
? deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
: deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
)
.then( .then(
(deletedIds) => { (deletedIds) => {
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@ -1800,7 +1810,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
size={tableContainerSizePx} size={tableContainerSizePx}
columnHeaders={columnHeaders} columnHeaders={columnHeaders}
isSelectionDisabled={ isSelectionDisabled={
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly (partitionKey.systemKey && !isPreferredApiMongoDB) ||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
} }
/> />
{tableItems.length > 0 && ( {tableItems.length > 0 && (

View File

@ -1,6 +1,5 @@
import { useMongoProxyEndpoint } from "Common/MongoProxyClient"; import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
import React, { Component } from "react"; import React, { Component } from "react";
import * as Constants from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext"; import { configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
@ -113,12 +112,6 @@ export default class MongoShellTabComponent extends Component<
const resourceId = databaseAccount?.id; const resourceId = databaseAccount?.id;
const accountName = databaseAccount?.name; const accountName = databaseAccount?.name;
const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint; const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint;
const mongoEndpoint =
documentEndpoint.substr(
Constants.MongoDBAccounts.protocol.length + 3,
documentEndpoint.length -
(Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length),
) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.props.collection.databaseId; const databaseId = this.props.collection.databaseId;
const collectionId = this.props.collection.id(); const collectionId = this.props.collection.id();
const apiEndpoint = this._useMongoProxyEndpoint const apiEndpoint = this._useMongoProxyEndpoint
@ -132,7 +125,7 @@ export default class MongoShellTabComponent extends Component<
data: { data: {
resourceId: resourceId, resourceId: resourceId,
accountName: accountName, accountName: accountName,
mongoEndpoint: mongoEndpoint, mongoEndpoint: documentEndpoint,
authorization: authorization, authorization: authorization,
databaseId: databaseId, databaseId: databaseId,
collectionId: collectionId, collectionId: collectionId,

View File

@ -51,13 +51,18 @@ export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: strin
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => { export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
const headers = new Headers(); const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString); headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
? configContext.PORTAL_BACKEND_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const url = backendEndpoint + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const response = await fetch(url, { headers, method: "POST" }); const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) { if (!response.ok) {
throw response; throw response;
} }
return (await response.text()) === "True"; return (await response.text()).toLowerCase() === "true";
}; };
export const ConnectExplorer: React.FunctionComponent<Props> = ({ export const ConnectExplorer: React.FunctionComponent<Props> = ({

View File

@ -164,6 +164,7 @@ export function useNewPortalBackendEndpoint(backendApi: string): boolean {
PortalBackendEndpoints.Mpac, PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod, PortalBackendEndpoints.Prod,
], ],
[BackendApi.AccountRestrictions]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
}; };
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) { if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {

View File

@ -52,11 +52,17 @@ export async function fetchDatabaseAccountsFromGraph(
const body = { const body = {
query: databaseAccountsQuery, query: databaseAccountsQuery,
subscriptions: [subscriptionId], subscriptions: [subscriptionId],
...(skipToken && { ...(skipToken
options: { ? {
$skipToken: skipToken, options: {
} as QueryRequestOptions, $skipToken: skipToken,
}), } as QueryRequestOptions,
}
: {
options: {
$top: 150,
} as QueryRequestOptions,
}),
}; };
const response = await fetch(managementResourceGraphAPIURL, { const response = await fetch(managementResourceGraphAPIURL, {

View File

@ -51,11 +51,17 @@ export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<
do { do {
const body = { const body = {
query: subscriptionsQuery, query: subscriptionsQuery,
...(skipToken && { ...(skipToken
options: { ? {
$skipToken: skipToken, options: {
} as QueryRequestOptions, $skipToken: skipToken,
}), } as QueryRequestOptions,
}
: {
options: {
$top: 150,
} as QueryRequestOptions,
}),
}; };
const response = await fetch(managementResourceGraphAPIURL, { const response = await fetch(managementResourceGraphAPIURL, {