mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-02-04 01:24:31 +00:00
Compare commits
66 Commits
ashleyst/r
...
users/sind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6473adf40 | ||
|
|
451316cad4 | ||
|
|
b456e53b2f | ||
|
|
ac2e2a6f8e | ||
|
|
3138580eae | ||
|
|
de5ba041e9 | ||
|
|
aa88815c6e | ||
|
|
ae7184f7ea | ||
|
|
5a2f78b51e | ||
|
|
3a6769280b | ||
|
|
fbc2e1335b | ||
|
|
4768ba3642 | ||
|
|
eb0d7b71b3 | ||
|
|
261289b031 | ||
|
|
fae4589427 | ||
|
|
cbcb7e6240 | ||
|
|
e0b773d920 | ||
|
|
9ec2cea95c | ||
|
|
1a4f713a79 | ||
|
|
bd564c665b | ||
|
|
7e5c6420ad | ||
|
|
89a3a040d8 | ||
|
|
ec3afa0526 | ||
|
|
4176a8a9a9 | ||
|
|
311cf9aa5a | ||
|
|
bc8094f44f | ||
|
|
9203276a24 | ||
|
|
d7825f4f78 | ||
|
|
e51c28c634 | ||
|
|
3b86a9477f | ||
|
|
521ff39eb0 | ||
|
|
2e2db3c2a9 | ||
|
|
2b84af60f4 | ||
|
|
40283ff7f1 | ||
|
|
29a1a819c3 | ||
|
|
5a16eec29d | ||
|
|
2b11e0e52b | ||
|
|
89374a16ba | ||
|
|
dc289ece75 | ||
|
|
1ddd372c6d | ||
|
|
2740657b4a | ||
|
|
8c888a751c | ||
|
|
8140f0edb1 | ||
|
|
ab5239df09 | ||
|
|
3e48393fbb | ||
|
|
0079a9147f | ||
|
|
912688dc14 | ||
|
|
8849526fab | ||
|
|
24af64a66d | ||
|
|
be871737ad | ||
|
|
4d8bb5c3ea | ||
|
|
10a8505b9a | ||
|
|
ef7c2fe2f7 | ||
|
|
4c7aca95e1 | ||
|
|
2243ad895a | ||
|
|
b2d5f91fe1 | ||
|
|
a712193477 | ||
|
|
5ee411693c | ||
|
|
16c7b2567b | ||
|
|
78d9a0cd8d | ||
|
|
c6ad538559 | ||
|
|
2bc09a6efe | ||
|
|
d3a3033b25 | ||
|
|
6bdc714e11 | ||
|
|
5042f28229 | ||
|
|
e1430fd06f |
@@ -18,7 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
|
||||
### Hosted Development (https://cosmos.azure.com)
|
||||
|
||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://cdb-ms-mpac-pbe.cosmos.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
|
||||
### Emulator Development
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</a>
|
||||
<ul>
|
||||
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
|
||||
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://main.documentdb.ext.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
|
||||
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://cdb-ms-mpac-pbe.cosmos.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
|
||||
</ul>
|
||||
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
||||
<h3>Emulator Development</h3>
|
||||
|
||||
@@ -4,7 +4,7 @@ const port = process.env.PORT || 3000;
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const api = createProxyMiddleware("/api", {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
logLevel: "debug",
|
||||
bypass: (req, res) => {
|
||||
@@ -16,7 +16,7 @@ const api = createProxyMiddleware("/api", {
|
||||
});
|
||||
|
||||
const proxy = createProxyMiddleware("/proxy", {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
|
||||
@@ -155,6 +155,18 @@ export class MongoProxyEndpoints {
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class MongoProxyApi {
|
||||
public static readonly ResourceList: string = "ResourceList";
|
||||
public static readonly QueryDocuments: string = "QueryDocuments";
|
||||
public static readonly CreateDocument: string = "CreateDocumen";
|
||||
public static readonly ReadDocument: string = "ReadDocument";
|
||||
public static readonly UpdateDocument: string = "UpdateDocument";
|
||||
public static readonly DeleteDocument: string = "DeleteDocument";
|
||||
public static readonly CreateCollectionWithProxy: string = "CreateCollectionWithProxy";
|
||||
public static readonly LegacyMongoShell: string = "LegacyMongoShell";
|
||||
public static readonly BulkDelete: string = "BulkDelete";
|
||||
}
|
||||
|
||||
export class CassandraProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7240";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { PortalBackendEndpoints } from "Common/Constants";
|
||||
import { configContext, Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
||||
|
||||
@@ -20,22 +21,22 @@ describe("getTokenFromAuthService", () => {
|
||||
|
||||
it("builds the correct URL in production", () => {
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
});
|
||||
getTokenFromAuthService("GET", "dbs", "foo");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://main.documentdb.ext.azure.com/api/guest/runtimeproxy/authorizationTokens",
|
||||
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct URL in dev", () => {
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
||||
});
|
||||
getTokenFromAuthService("GET", "dbs", "foo");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
|
||||
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -78,7 +79,7 @@ describe("requestPlugin", () => {
|
||||
const next = jest.fn();
|
||||
updateConfigContext({
|
||||
platform: Platform.Hosted,
|
||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
PORTAL_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
PROXY_PATH: "/proxy",
|
||||
});
|
||||
const headers = {};
|
||||
|
||||
@@ -8,11 +8,13 @@ import { AuthType } from "../AuthType";
|
||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||
import { runCommand } from "hooks/useDatabaseAccounts";
|
||||
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||
|
||||
const _global = typeof self === "undefined" ? window : self;
|
||||
|
||||
@@ -27,12 +29,47 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
);
|
||||
if (!userContext.aadToken) {
|
||||
logConsoleError(
|
||||
`AAD token does not exist. Please click on "Login for Entra ID" button prior to performing Entra ID RBAC operations`,
|
||||
`AAD token does not exist. Please use the "Login for Entra ID" button in the Toolbar prior to performing Entra ID RBAC operations`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
||||
let authorizationToken;
|
||||
|
||||
try {
|
||||
authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
||||
} catch (error) {
|
||||
if (error.code === "ExpiredAuthenticationToken") {
|
||||
// Renew the AAD token using runCommand
|
||||
const newToken = await runCommand(async () => {
|
||||
// Implement the logic to acquire a new AAD token
|
||||
const msalInstance = await getMsalInstance();
|
||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
|
||||
msalInstance.setActiveAccount(cachedAccount);
|
||||
const newAccessToken = await acquireTokenWithMsal(msalInstance, {
|
||||
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
|
||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||
});
|
||||
|
||||
// Update user context with the new token
|
||||
updateUserContext({ aadToken: newAccessToken });
|
||||
authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
||||
return newAccessToken;
|
||||
});
|
||||
|
||||
// Retry getting the token after renewing
|
||||
const retryResult = await getTokenFromAuthService(verb, resourceType, resourceId);
|
||||
headers[HttpHeaders.msDate] = retryResult.XDate;
|
||||
return decodeURIComponent(retryResult.PrimaryReadWriteToken);
|
||||
} else {
|
||||
console.error('An error occurred:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return authorizationToken;
|
||||
}
|
||||
|
||||
|
||||
3
src/Common/DatabaseUtility.ts
Normal file
3
src/Common/DatabaseUtility.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function getNewDatabaseSharedThroughputDefault(): boolean {
|
||||
return false;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface TableEntityProps {
|
||||
isEntityValueDisable?: boolean;
|
||||
entityTimeValue: string;
|
||||
entityValueType: string;
|
||||
entityProperty: string;
|
||||
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||
onSelectDate: (date: Date | null | undefined) => void;
|
||||
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||
@@ -26,6 +27,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
onSelectDate,
|
||||
isEntityValueDisable,
|
||||
onEntityTimeValueChange,
|
||||
entityProperty,
|
||||
}: TableEntityProps): JSX.Element => {
|
||||
if (isEntityTypeDate) {
|
||||
return (
|
||||
@@ -51,15 +53,20 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label={entityValueLabel && entityValueLabel}
|
||||
className="addEntityTextField"
|
||||
disabled={isEntityValueDisable}
|
||||
type={entityValueType}
|
||||
placeholder={entityValuePlaceholder}
|
||||
value={typeof entityValue === "string" ? entityValue : ""}
|
||||
onChange={onEntityValueChange}
|
||||
ariaLabel={attributeValueLabel}
|
||||
/>
|
||||
<>
|
||||
<span id={entityProperty} className="screenReaderOnly">
|
||||
Edit Property {entityProperty} {attributeValueLabel}
|
||||
</span>
|
||||
<TextField
|
||||
label={entityValueLabel && entityValueLabel}
|
||||
className="addEntityTextField"
|
||||
disabled={isEntityValueDisable}
|
||||
type={entityValueType}
|
||||
placeholder={entityValuePlaceholder}
|
||||
value={typeof entityValue === "string" ? entityValue : ""}
|
||||
onChange={onEntityValueChange}
|
||||
aria-labelledby={entityProperty}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MongoProxyEndpoints } from "Common/Constants";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
@@ -71,7 +72,7 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -82,16 +83,16 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
queryDocuments(databaseId, collection, true, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" });
|
||||
queryDocuments(databaseId, collection, true, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -103,7 +104,7 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -114,16 +115,16 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" });
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -135,7 +136,7 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -146,16 +147,16 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" });
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -167,7 +168,7 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -178,7 +179,7 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -187,7 +188,7 @@ describe("MongoProxyClient", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -199,7 +200,7 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -210,16 +211,16 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct URL", () => {
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" });
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -231,13 +232,13 @@ describe("MongoProxyClient", () => {
|
||||
databaseAccount,
|
||||
});
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
@@ -249,18 +250,19 @@ describe("MongoProxyClient", () => {
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
});
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFeatureEndpointOrDefault", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
||||
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||
});
|
||||
const features = extractFeatures(params);
|
||||
@@ -272,12 +274,12 @@ describe("MongoProxyClient", () => {
|
||||
|
||||
it("returns a local endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import {
|
||||
allowedMongoProxyEndpoints,
|
||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
import queryString from "querystring";
|
||||
@@ -14,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@@ -67,7 +67,7 @@ export function queryDocuments(
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export function queryDocuments(
|
||||
query,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -194,7 +194,7 @@ export function readDocument(
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint("readDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -217,7 +217,7 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -289,7 +289,7 @@ export function createDocument(
|
||||
partitionKeyProperty: string,
|
||||
documentContent: unknown,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint("createDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -308,7 +308,7 @@ export function createDocument(
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createDocument`, {
|
||||
@@ -373,7 +373,7 @@ export function updateDocument(
|
||||
documentId: DocumentId,
|
||||
documentContent: string,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint("updateDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -396,7 +396,7 @@ export function updateDocument(
|
||||
: "",
|
||||
documentContent,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -464,7 +464,7 @@ export function updateDocument_ToBeDeprecated(
|
||||
}
|
||||
|
||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||
if (!useMongoProxyEndpoint("deleteDocument")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) {
|
||||
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -486,7 +486,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -561,7 +561,10 @@ export function deleteDocuments(
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
|
||||
const rids = documentIds.map((documentId) => documentId.id());
|
||||
const rids: string[] = documentIds.map((documentId) => {
|
||||
const idComponents = documentId.self.split("/");
|
||||
return idComponents[5];
|
||||
});
|
||||
|
||||
const params = {
|
||||
databaseID: databaseId,
|
||||
@@ -572,7 +575,7 @@ export function deleteDocuments(
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/bulkdelete`, {
|
||||
@@ -596,7 +599,7 @@ export function deleteDocuments(
|
||||
export function createMongoCollectionWithProxy(
|
||||
params: DataModels.CreateCollectionParams,
|
||||
): Promise<DataModels.Collection> {
|
||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
@@ -619,7 +622,7 @@ export function createMongoCollectionWithProxy(
|
||||
isSharded: !!shardKey,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createCollection`, {
|
||||
@@ -686,15 +689,16 @@ export function createMongoCollectionWithProxy_ToBeDeprecated(
|
||||
}
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
let endpoint;
|
||||
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
||||
...defaultAllowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
];
|
||||
if (useMongoProxyEndpoint(feature)) {
|
||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
} else {
|
||||
endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
||||
...allowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
])
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
@@ -715,19 +719,78 @@ export function getEndpoint(endpoint: string): string {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
];
|
||||
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||
[MongoProxyApi.ResourceList]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.QueryDocuments]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.ReadDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.UpdateDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.DeleteDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.LegacyMongoShell]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.BulkDelete]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||
);
|
||||
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
||||
}
|
||||
|
||||
export class ThrottlingError extends Error {
|
||||
|
||||
@@ -135,6 +135,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
onEntityValueChange={onEntityValueChange}
|
||||
onSelectDate={onSelectDate}
|
||||
onEntityTimeValueChange={onEntityTimeValueChange}
|
||||
entityProperty={entityProperty}
|
||||
/>
|
||||
{!isEntityValueDisable && (
|
||||
<TooltipHost content="Edit property" id="editTooltip">
|
||||
|
||||
@@ -5,19 +5,20 @@ import {
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import { userContext } from "UserContext";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedCassandraProxyEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMongoProxyEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
defaultAllowedCassandraProxyEndpoints,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
|
||||
@@ -32,10 +33,13 @@ export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedArmEndpoints: ReadonlyArray<string>;
|
||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
ENVIRONMENT: string;
|
||||
ARM_AUTH_AREA: string;
|
||||
ARM_ENDPOINT: string;
|
||||
EMULATOR_ENDPOINT?: string;
|
||||
@@ -53,7 +57,6 @@ export interface ConfigContext {
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT: string;
|
||||
NEW_MONGO_APIS?: string[];
|
||||
CASSANDRA_PROXY_ENDPOINT: string;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
PROXY_PATH?: string;
|
||||
@@ -73,9 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
platform: Platform.Portal,
|
||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/cdb-(ms|ff|mc)-prod-pbe\\.cosmos\\.azure\\.(com|us|cn)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
@@ -89,7 +95,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
AAD_ENDPOINT: "",
|
||||
ARM_AUTH_AREA: "https://management.azure.com/",
|
||||
ARM_ENDPOINT: "https://management.azure.com/",
|
||||
ARM_API_VERSION: "2016-06-01",
|
||||
@@ -106,17 +112,6 @@ let configContext: Readonly<ConfigContext> = {
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
NEW_MONGO_APIS: [
|
||||
"resourcelist",
|
||||
"queryDocuments",
|
||||
"createDocument",
|
||||
"readDocument",
|
||||
"updateDocument",
|
||||
"deleteDocument",
|
||||
"createCollectionWithProxy",
|
||||
"legacyMongoShell",
|
||||
// "bulkdelete",
|
||||
],
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||
isTerminalEnabled: false,
|
||||
@@ -164,7 +159,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.MONGO_PROXY_ENDPOINT,
|
||||
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
||||
)
|
||||
) {
|
||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
|
||||
)
|
||||
) {
|
||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,6 +381,7 @@ export enum TerminalKind {
|
||||
export interface DataExplorerInputsFrame {
|
||||
databaseAccount: any;
|
||||
subscriptionId?: string;
|
||||
tenantId?: string;
|
||||
resourceGroup?: string;
|
||||
masterKey?: string;
|
||||
hasWriteAccess?: boolean;
|
||||
|
||||
@@ -56,13 +56,15 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
),
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
});
|
||||
@@ -146,14 +148,15 @@ export const createCollectionContextMenuButton = (
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () => {
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useCallback } from "react";
|
||||
|
||||
export interface TreeNodeMenuItem {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
onClick: (value?: React.RefObject<HTMLElement>) => void;
|
||||
iconSrc?: string;
|
||||
isDisabled?: boolean;
|
||||
styleClass?: string;
|
||||
@@ -74,6 +74,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
openItems,
|
||||
}: TreeNodeComponentProps): JSX.Element => {
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const contextMenuRef = React.useRef<HTMLButtonElement>(null);
|
||||
const treeStyles = useTreeStyles();
|
||||
|
||||
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
||||
@@ -141,7 +142,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
||||
disabled={menuItem.isDisabled}
|
||||
key={menuItem.label}
|
||||
onClick={menuItem.onClick}
|
||||
onClick={() => menuItem.onClick(contextMenuRef)}
|
||||
>
|
||||
{menuItem.label}
|
||||
</MenuItem>
|
||||
@@ -190,6 +191,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
||||
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
||||
data-test="TreeNode/ContextMenuTrigger"
|
||||
appearance="subtle"
|
||||
ref={contextMenuRef}
|
||||
icon={<MoreHorizontal20Regular />}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
|
||||
@@ -1478,14 +1478,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||
onClick={[MockFunction enabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
enabledItem
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||
disabled={true}
|
||||
onClick={[MockFunction disabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
disabledItem
|
||||
</MenuItem>
|
||||
@@ -1518,7 +1518,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
<MenuItem
|
||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||
key="enabledItem"
|
||||
onClick={[MockFunction enabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
enabledItem
|
||||
</MenuItem>
|
||||
@@ -1526,7 +1526,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||
disabled={true}
|
||||
key="disabledItem"
|
||||
onClick={[MockFunction disabledItemClick]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
disabledItem
|
||||
</MenuItem>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
@@ -259,25 +259,8 @@ export default class Explorer {
|
||||
|
||||
public async openLoginForEntraIDPopUp(): Promise<void> {
|
||||
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
||||
/\/$/,
|
||||
"/.default",
|
||||
);
|
||||
const msalInstance = await getMsalInstance();
|
||||
|
||||
try {
|
||||
const response = await msalInstance.loginPopup({
|
||||
redirectUri: configContext.msalRedirectURI,
|
||||
scopes: [],
|
||||
});
|
||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||
msalInstance.setActiveAccount(cachedAccount);
|
||||
const aadToken = await acquireTokenWithMsal(msalInstance, {
|
||||
forceRefresh: true,
|
||||
scopes: [hrefEndpoint],
|
||||
authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`,
|
||||
});
|
||||
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false);
|
||||
updateUserContext({ aadToken: aadToken });
|
||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
@@ -125,7 +125,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
createNewDatabase:
|
||||
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||
isSharedThroughputChecked: getNewDatabaseSharedThroughputDefault(),
|
||||
selectedDatabaseId:
|
||||
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||
@@ -1138,10 +1138,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||
}
|
||||
|
||||
private getSharedThroughputDefault(): boolean {
|
||||
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
|
||||
}
|
||||
|
||||
private getFreeTierIndexingText(): string {
|
||||
return this.state.enableIndexing
|
||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
@@ -48,7 +49,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
|
||||
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
||||
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
||||
subscriptionType !== SubscriptionType.EA && !isServerlessAccount(),
|
||||
getNewDatabaseSharedThroughputDefault(),
|
||||
);
|
||||
const [formErrors, setFormErrors] = useState<string>("");
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
|
||||
@@ -65,7 +65,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={true}
|
||||
checked={false}
|
||||
label="Provision throughput"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
@@ -90,14 +90,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
</InfoTooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={true}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
`;
|
||||
|
||||
@@ -124,7 +124,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
message:
|
||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||
};
|
||||
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
|
||||
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id (name)`;
|
||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
@@ -132,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
|
||||
<Text variant="small">{confirmDatabase}</Text>
|
||||
<TextField
|
||||
id="confirmDatabaseId"
|
||||
data-test="Input:confirmDatabaseId"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
AuthError as msalAuthError,
|
||||
BrowserAuthErrorMessage as msalBrowserAuthErrorMessage,
|
||||
} from "@azure/msal-browser";
|
||||
import {
|
||||
Checkbox,
|
||||
ChoiceGroup,
|
||||
@@ -5,8 +9,6 @@ import {
|
||||
IChoiceGroupOption,
|
||||
ISpinButtonStyles,
|
||||
IToggleStyles,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Position,
|
||||
SpinButton,
|
||||
Toggle,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
} from "Shared/StorageUtility";
|
||||
import * as StringUtility from "Shared/StringUtility";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
@@ -108,7 +111,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
|
||||
: Constants.RBACOptions.setAutomaticRBACOption,
|
||||
);
|
||||
const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState<boolean>(false);
|
||||
|
||||
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
|
||||
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
|
||||
@@ -203,6 +205,24 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
hasDataPlaneRbacSettingChanged: true,
|
||||
});
|
||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||
try {
|
||||
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, true);
|
||||
updateUserContext({ aadToken: aadToken });
|
||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||
} catch (authError) {
|
||||
if (
|
||||
authError instanceof msalAuthError &&
|
||||
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
|
||||
) {
|
||||
logConsoleError(
|
||||
`We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on "Login for Entra ID" button`,
|
||||
);
|
||||
} else {
|
||||
logConsoleError(
|
||||
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: false,
|
||||
@@ -347,13 +367,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
option: IChoiceGroupOption,
|
||||
): void => {
|
||||
setEnableDataPlaneRBACOption(option.key);
|
||||
|
||||
const shouldShowWarning =
|
||||
(option.key === Constants.RBACOptions.setTrueRBACOption ||
|
||||
(option.key === Constants.RBACOptions.setAutomaticRBACOption &&
|
||||
userContext.databaseAccount.properties.disableLocalAuth === true)) &&
|
||||
!useDataPlaneRbac.getState().aadTokenUpdated;
|
||||
setShowDataPlaneRBACWarning(shouldShowWarning);
|
||||
};
|
||||
|
||||
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||
@@ -528,17 +541,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
isMultiline={true}
|
||||
onDismiss={() => setShowDataPlaneRBACWarning(false)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
>
|
||||
Please click on "Login for Entra ID RBAC" button prior to performing Entra ID RBAC
|
||||
operations
|
||||
</MessageBar>
|
||||
)}
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||
ID RBAC.
|
||||
|
||||
@@ -106,7 +106,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={true}
|
||||
checked={false}
|
||||
label="Share throughput across containers"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
@@ -137,14 +137,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={true}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
@@ -263,6 +255,14 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
|
||||
@@ -361,13 +361,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
<span
|
||||
className="css-113"
|
||||
>
|
||||
Confirm by typing the
|
||||
Database
|
||||
id
|
||||
Confirm by typing the Database id (name)
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Confirm by typing the Database id"
|
||||
ariaLabel="Confirm by typing the Database id (name)"
|
||||
autoFocus={true}
|
||||
data-test="Input:confirmDatabaseId"
|
||||
id="confirmDatabaseId"
|
||||
@@ -382,7 +380,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
}
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Confirm by typing the Database id"
|
||||
ariaLabel="Confirm by typing the Database id (name)"
|
||||
autoFocus={true}
|
||||
data-test="Input:confirmDatabaseId"
|
||||
deferredValidationTime={200}
|
||||
@@ -677,7 +675,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Confirm by typing the Database id"
|
||||
aria-label="Confirm by typing the Database id (name)"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-117"
|
||||
data-test="Input:confirmDatabaseId"
|
||||
|
||||
@@ -2115,7 +2115,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
||||
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
||||
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint("bulkdelete");
|
||||
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete);
|
||||
const isBulkDeleteDisabled =
|
||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||
// -------------------------------------------------------
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
TextSortDescendingRegular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { NormalizedEventKey } from "Common/Constants";
|
||||
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
||||
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
||||
import {
|
||||
ColumnSizesMap,
|
||||
@@ -228,31 +227,29 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
||||
Refresh
|
||||
</MenuItem>
|
||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) && (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={<TextSortAscendingRegular />}
|
||||
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
||||
>
|
||||
Sort ascending
|
||||
<>
|
||||
<MenuItem
|
||||
icon={<TextSortAscendingRegular />}
|
||||
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
||||
>
|
||||
Sort ascending
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<TextSortDescendingRegular />}
|
||||
onClick={(e) => onSortClick(e, column.id, "descending")}
|
||||
>
|
||||
Sort descending
|
||||
</MenuItem>
|
||||
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
||||
Reset sorting
|
||||
</MenuItem>
|
||||
{!isColumnSelectionDisabled && (
|
||||
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
||||
Edit columns
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<TextSortDescendingRegular />}
|
||||
onClick={(e) => onSortClick(e, column.id, "descending")}
|
||||
>
|
||||
Sort descending
|
||||
</MenuItem>
|
||||
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
||||
Reset sorting
|
||||
</MenuItem>
|
||||
{!isColumnSelectionDisabled && (
|
||||
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
||||
Edit columns
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
<MenuDivider />
|
||||
</>
|
||||
<MenuItem
|
||||
key="keyboardresize"
|
||||
icon={<TableResizeColumnRegular />}
|
||||
@@ -260,25 +257,24 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
>
|
||||
Resize with left/right arrow keys
|
||||
</MenuItem>
|
||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) &&
|
||||
!isColumnSelectionDisabled && (
|
||||
<MenuItem
|
||||
key="remove"
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => {
|
||||
// Remove column id from selectedColumnIds
|
||||
const index = selectedColumnIds.indexOf(column.id);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
const newSelectedColumnIds = [...selectedColumnIds];
|
||||
newSelectedColumnIds.splice(index, 1);
|
||||
onColumnSelectionChange(newSelectedColumnIds);
|
||||
}}
|
||||
>
|
||||
Remove column
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isColumnSelectionDisabled && (
|
||||
<MenuItem
|
||||
key="remove"
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => {
|
||||
// Remove column id from selectedColumnIds
|
||||
const index = selectedColumnIds.indexOf(column.id);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
const newSelectedColumnIds = [...selectedColumnIds];
|
||||
newSelectedColumnIds.splice(index, 1);
|
||||
onColumnSelectionChange(newSelectedColumnIds);
|
||||
}}
|
||||
>
|
||||
Remove column
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
constructor(props: IMongoShellTabComponentProps) {
|
||||
super(props);
|
||||
this._logTraces = new Map();
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
||||
|
||||
this.state = {
|
||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||
|
||||
@@ -83,8 +83,8 @@ const bindings: Record<KeyboardAction, string[]> = {
|
||||
[KeyboardAction.NEW_ITEM]: ["Alt+N I"],
|
||||
[KeyboardAction.DELETE_ITEM]: ["Alt+D"],
|
||||
[KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"],
|
||||
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
|
||||
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
|
||||
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+BracketLeft", "$mod+Shift+F6"],
|
||||
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+BracketRight", "$mod+F6"],
|
||||
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
|
||||
[KeyboardAction.SEARCH]: ["$mod+Shift+F"],
|
||||
[KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"],
|
||||
|
||||
@@ -74,11 +74,13 @@ export interface UserContext {
|
||||
readonly authType?: AuthType;
|
||||
readonly masterKey?: string;
|
||||
readonly subscriptionId?: string;
|
||||
readonly tenantId?: string;
|
||||
readonly resourceGroup?: string;
|
||||
readonly databaseAccount?: DatabaseAccount;
|
||||
readonly endpoint?: string;
|
||||
readonly aadToken?: string;
|
||||
readonly accessToken?: string;
|
||||
readonly armToken?: string;
|
||||
readonly authorizationToken?: string;
|
||||
readonly resourceToken?: string;
|
||||
readonly subscriptionType?: SubscriptionType;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as msal from "@azure/msal-browser";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
@@ -64,7 +65,83 @@ export async function getMsalInstance() {
|
||||
return msalInstance;
|
||||
}
|
||||
|
||||
export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) {
|
||||
export async function acquireMsalTokenForAccount(
|
||||
account: DatabaseAccount,
|
||||
silent: boolean = false,
|
||||
user_hint?: string,
|
||||
) {
|
||||
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
||||
throw new Error("Database account has no document endpoint defined");
|
||||
}
|
||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
||||
/\/+$/,
|
||||
"/.default",
|
||||
);
|
||||
const msalInstance = await getMsalInstance();
|
||||
const knownAccounts = msalInstance.getAllAccounts();
|
||||
// If user_hint is provided, we will try to use it to find the account.
|
||||
// If no account is found, we will use the current active account or first account in the list.
|
||||
const msalAccount =
|
||||
knownAccounts?.filter((account) => account.username === user_hint)[0] ??
|
||||
msalInstance.getActiveAccount() ??
|
||||
knownAccounts?.[0];
|
||||
|
||||
if (!msalAccount) {
|
||||
// If no account was found, we need to sign in.
|
||||
// This will eventually throw InteractionRequiredAuthError if silent is true, we won't handle it here.
|
||||
const loginRequest = {
|
||||
scopes: [hrefEndpoint],
|
||||
loginHint: user_hint,
|
||||
};
|
||||
try {
|
||||
if (silent) {
|
||||
// We can try to use SSO between different apps to avoid showing a popup.
|
||||
// With a hint provided, this should work in most cases.
|
||||
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-sso#sso-between-different-apps
|
||||
try {
|
||||
const loginResponse = await msalInstance.ssoSilent(loginRequest);
|
||||
return loginResponse.accessToken;
|
||||
} catch (silentError) {
|
||||
trace(Action.SignInAad, ActionModifiers.Mark, {
|
||||
request: JSON.stringify(loginRequest),
|
||||
acquireTokenType: silent ? "silent" : "interactive",
|
||||
errorMessage: JSON.stringify(silentError),
|
||||
});
|
||||
}
|
||||
}
|
||||
// If silent acquisition failed, we need to show a popup.
|
||||
// Passing prompt: "none" will still show a popup but not perform a full sign-in.
|
||||
// This will only work if the user has already signed in and the session is still valid.
|
||||
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-prompt-behavior#interactive-requests-with-promptnone
|
||||
// The hint will be used to pre-fill the username field in the popup if silent is false.
|
||||
const loginResponse = await msalInstance.loginPopup({ prompt: silent ? "none" : "login", ...loginRequest });
|
||||
return loginResponse.accessToken;
|
||||
} catch (error) {
|
||||
traceFailure(Action.SignInAad, {
|
||||
request: JSON.stringify(loginRequest),
|
||||
acquireTokenType: silent ? "silent" : "interactive",
|
||||
errorMessage: JSON.stringify(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
msalInstance.setActiveAccount(msalAccount);
|
||||
}
|
||||
|
||||
const tokenRequest = {
|
||||
account: msalAccount || null,
|
||||
forceRefresh: true,
|
||||
scopes: [hrefEndpoint],
|
||||
authority: `${configContext.AAD_ENDPOINT}${msalAccount.tenantId}`,
|
||||
};
|
||||
return acquireTokenWithMsal(msalInstance, tokenRequest, silent);
|
||||
}
|
||||
|
||||
export async function acquireTokenWithMsal(
|
||||
msalInstance: msal.IPublicClientApplication,
|
||||
request: msal.SilentRequest,
|
||||
silent: boolean = false,
|
||||
) {
|
||||
const tokenRequest = {
|
||||
account: msalInstance.getActiveAccount() || null,
|
||||
...request,
|
||||
@@ -74,7 +151,7 @@ export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientAppli
|
||||
// attempt silent acquisition first
|
||||
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
|
||||
} catch (silentError) {
|
||||
if (silentError instanceof msal.InteractionRequiredAuthError) {
|
||||
if (silentError instanceof msal.InteractionRequiredAuthError && silent === false) {
|
||||
try {
|
||||
// The error indicates that we need to acquire the token interactively.
|
||||
// This will display a pop-up to re-establish authorization. If user does not
|
||||
|
||||
@@ -92,7 +92,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
||||
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
|
||||
};
|
||||
|
||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
@@ -108,7 +108,7 @@ export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> =
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const allowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
|
||||
@@ -13,9 +13,9 @@ describe("isInvalidParentFrameOrigin", () => {
|
||||
${"https://subdomain.portal.azure.com"} | ${false}
|
||||
${"https://subdomain.portal.azure.us"} | ${false}
|
||||
${"https://subdomain.portal.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.com"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.us"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.cn"} | ${false}
|
||||
${"https://cdb-ms-prod-pbe.cosmos.azure.com"} | ${false}
|
||||
${"https://cdb-ff-prod-pbe.cosmos.azure.us"} | ${false}
|
||||
${"https://cdb-mc-prod-pbe.cosmos.azure.cn"} | ${false}
|
||||
${"https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de"} | ${false}
|
||||
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
|
||||
${"https://random.domain"} | ${true}
|
||||
|
||||
@@ -2,12 +2,11 @@ import { MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import { resetConfigContext, updateConfigContext } from "ConfigContext";
|
||||
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
|
||||
import { updateUserContext } from "UserContext";
|
||||
import { MongoProxyOutboundIPs, PortalBackendIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
|
||||
import { MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
|
||||
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
|
||||
|
||||
describe("NetworkUtility tests", () => {
|
||||
describe("getNetworkSettingsWarningMessage", () => {
|
||||
const legacyBackendEndpoint: string = "https://main.documentdb.ext.azure.com";
|
||||
const publicAccessMessagePart = "Please enable public access to proceed";
|
||||
const accessMessagePart = "Please allow access from Azure Portal to proceed";
|
||||
let warningMessageResult: string;
|
||||
@@ -48,25 +47,23 @@ describe("NetworkUtility tests", () => {
|
||||
});
|
||||
|
||||
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, async () => {
|
||||
const portalBackendOutboundIPsWithLegacyIPs: string[] = [
|
||||
const portalBackendOutboundIPs: string[] = [
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||
...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac],
|
||||
...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod],
|
||||
...PortalBackendIPs["https://main.documentdb.ext.azure.com"],
|
||||
];
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
kind: "MongoDB",
|
||||
properties: {
|
||||
ipRules: portalBackendOutboundIPsWithLegacyIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
|
||||
ipRules: portalBackendOutboundIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
|
||||
publicNetworkAccess: "Enabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: legacyBackendEndpoint,
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
});
|
||||
@@ -90,7 +87,6 @@ describe("NetworkUtility tests", () => {
|
||||
});
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: legacyBackendEndpoint,
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
});
|
||||
|
||||
@@ -2,12 +2,7 @@ import { CassandraProxyEndpoints, MongoProxyEndpoints, PortalBackendEndpoints }
|
||||
import { configContext } from "ConfigContext";
|
||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import { userContext } from "UserContext";
|
||||
import {
|
||||
CassandraProxyOutboundIPs,
|
||||
MongoProxyOutboundIPs,
|
||||
PortalBackendIPs,
|
||||
PortalBackendOutboundIPs,
|
||||
} from "Utils/EndpointUtils";
|
||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
|
||||
|
||||
export const getNetworkSettingsWarningMessage = async (
|
||||
setStateFunc: (warningMessage: string) => void,
|
||||
@@ -61,7 +56,7 @@ export const getNetworkSettingsWarningMessage = async (
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||
]
|
||||
: PortalBackendOutboundIPs[configContext.PORTAL_BACKEND_ENDPOINT];
|
||||
let portalIPs: string[] = [...portalBackendOutboundIPs, ...PortalBackendIPs[configContext.BACKEND_ENDPOINT]];
|
||||
let portalIPs: string[] = [...portalBackendOutboundIPs];
|
||||
|
||||
if (userContext.apiType === "Mongo") {
|
||||
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useBoolean } from "@fluentui/react-hooks";
|
||||
import * as React from "react";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
import { updateUserContext } from "UserContext";
|
||||
|
||||
const msalInstance = await getMsalInstance();
|
||||
|
||||
@@ -79,7 +80,7 @@ export function useAADAuth(): ReturnType {
|
||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||
});
|
||||
|
||||
updateUserContext({ armToken: armToken});
|
||||
setArmToken(armToken);
|
||||
setAuthFailure(null);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
|
||||
import useSWR from "swr";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||
import React from "react";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface AccountListResult {
|
||||
@@ -34,11 +34,10 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
|
||||
}
|
||||
|
||||
export async function fetchDatabaseAccountsFromGraph(
|
||||
subscriptionId: string,
|
||||
accessToken: string,
|
||||
subscriptionId: string
|
||||
): Promise<DatabaseAccount[]> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
const bearer = `Bearer ${userContext.armToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
headers.append(HttpHeaders.contentType, "application/json");
|
||||
@@ -46,8 +45,9 @@ export async function fetchDatabaseAccountsFromGraph(
|
||||
const apiVersion = "2021-03-01";
|
||||
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
|
||||
|
||||
const databaseAccounts: DatabaseAccount[] = [];
|
||||
let databaseAccounts: DatabaseAccount[] = [];
|
||||
let skipToken: string;
|
||||
console.log("Old ARM Token", userContext.armToken);
|
||||
do {
|
||||
const body = {
|
||||
query: databaseAccountsQuery,
|
||||
@@ -74,21 +74,166 @@ export async function fetchDatabaseAccountsFromGraph(
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
|
||||
const queryResponse: QueryResponse = (await response.json()) as QueryResponse;
|
||||
skipToken = queryResponse.$skipToken;
|
||||
queryResponse.data?.map((databaseAccount: any) => {
|
||||
databaseAccounts.push(databaseAccount as DatabaseAccount);
|
||||
});
|
||||
|
||||
// else {
|
||||
// try{
|
||||
// console.log("Token expired");
|
||||
// databaseAccounts = await acquireNewTokenAndRetry(body);
|
||||
// }
|
||||
// catch (error) {
|
||||
// throw new Error(error);
|
||||
// }
|
||||
|
||||
//}
|
||||
} while (skipToken);
|
||||
|
||||
return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined {
|
||||
export function useDatabaseAccounts(subscriptionId: string): DatabaseAccount[] | undefined {
|
||||
const { data } = useSWR(
|
||||
() => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined),
|
||||
(_, subscriptionId, armToken) => fetchDatabaseAccountsFromGraph(subscriptionId, armToken),
|
||||
() => ( subscriptionId ? ["databaseAccounts", subscriptionId] : undefined),
|
||||
(_, subscriptionId) => runCommand(fetchDatabaseAccountsFromGraph, subscriptionId),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Define the types for your responses
|
||||
interface DatabaseAccount {
|
||||
name: string;
|
||||
id: string;
|
||||
// Add other relevant fields as per your use case
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
displayName: string;
|
||||
subscriptionId: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
interface QueryRequestOptions {
|
||||
$top?: number;
|
||||
$skipToken?: string;
|
||||
$allowPartialScopes?: boolean;
|
||||
}
|
||||
|
||||
// Define the configuration context and headers if not already defined
|
||||
const configContext = {
|
||||
ARM_ENDPOINT: 'https://management.azure.com/',
|
||||
AAD_ENDPOINT: 'https://login.microsoftonline.com/'
|
||||
};
|
||||
|
||||
interface QueryResponse {
|
||||
data?: any[];
|
||||
$skipToken?: string;
|
||||
}
|
||||
|
||||
export async function runCommand<T>(
|
||||
fn: (...args: any[]) => Promise<T>,
|
||||
...args: any[]
|
||||
): Promise<T> {
|
||||
try {
|
||||
// Attempt to execute the function passed as an argument
|
||||
const result = await fn(...args);
|
||||
console.log('Successfully executed function:', result);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
// Handle any error that is thrown during the execution of the function
|
||||
//(error.code === "ExpiredAuthenticationToken")
|
||||
if(error) {
|
||||
console.log('Creating new token');
|
||||
const msalInstance = await getMsalInstance();
|
||||
|
||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
|
||||
|
||||
msalInstance.setActiveAccount(cachedAccount);
|
||||
|
||||
const newAccessToken = await acquireTokenWithMsal(msalInstance, {
|
||||
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
|
||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||
});
|
||||
|
||||
console.log("Latest ARM Token", userContext.armToken);
|
||||
updateUserContext({armToken: newAccessToken});
|
||||
const result = await fn(...args);
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
console.error('An error occurred:', error.message);
|
||||
throw new error;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Running the functions using runCommand
|
||||
|
||||
const accessToken = 'your-access-token';
|
||||
const subscriptionId = 'your-subscription-id';
|
||||
|
||||
//runCommand(fetchDatabaseAccountsFromGraph, subscriptionId, accessToken);
|
||||
//runCommand(fetchSubscriptionsFromGraph, accessToken);
|
||||
|
||||
async function acquireNewTokenAndRetry(body: any) : Promise<DatabaseAccount[]> {
|
||||
try {
|
||||
const msalInstance = await getMsalInstance();
|
||||
|
||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
|
||||
// const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
||||
|
||||
|
||||
msalInstance.setActiveAccount(cachedAccount);
|
||||
|
||||
const newAccessToken = await acquireTokenWithMsal(msalInstance, {
|
||||
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
|
||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||
});
|
||||
console.log("New ARM Token", newAccessToken);
|
||||
const newBearer = `Bearer ${newAccessToken}`;
|
||||
const newHeaders = new Headers();
|
||||
newHeaders.append("Authorization", newBearer);
|
||||
newHeaders.append(HttpHeaders.contentType, "application/json");
|
||||
const apiVersion = "2021-03-01";
|
||||
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
|
||||
|
||||
const databaseAccounts: DatabaseAccount[] = [];
|
||||
let skipToken: string;
|
||||
|
||||
|
||||
// Retry the request with the new token
|
||||
const response = await fetch(managementResourceGraphAPIURL, {
|
||||
method: "POST",
|
||||
headers: newHeaders,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Handle successful response with new token
|
||||
const queryResponse: QueryResponse = await response.json();
|
||||
skipToken = queryResponse.$skipToken;
|
||||
queryResponse.data?.forEach((databaseAccount: any) => {
|
||||
databaseAccounts.push(databaseAccount as DatabaseAccount);
|
||||
});
|
||||
return databaseAccounts;
|
||||
} else {
|
||||
throw new Error(`Failed to fetch data after acquiring new token. Status: ${response.status}, ${await response.text()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error acquiring new token and retrying:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
@@ -18,6 +19,7 @@ import { AuthType } from "../AuthType";
|
||||
import { AccountKind, Flights } from "../Common/Constants";
|
||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
||||
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
|
||||
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
|
||||
@@ -41,7 +43,12 @@ import {
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||
import { acquireTokenWithMsal, getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
import {
|
||||
acquireMsalTokenForAccount,
|
||||
acquireTokenWithMsal,
|
||||
getAuthorizationHeader,
|
||||
getMsalInstance,
|
||||
} from "../Utils/AuthorizationUtils";
|
||||
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
|
||||
import { getReadOnlyKeys, listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||
@@ -459,6 +466,7 @@ export async function fetchAndUpdateKeys(subscriptionId: string, resourceGroup:
|
||||
Logger.logInfo(`Fetching keys for ${userContext.apiType} account ${account}`, "Explorer/fetchAndUpdateKeys");
|
||||
let keys;
|
||||
try {
|
||||
keys = await listKeys(subscriptionId, resourceGroup, account);
|
||||
keys = await listKeys(subscriptionId, resourceGroup, account);
|
||||
Logger.logInfo(`Keys fetched for ${userContext.apiType} account ${account}`, "Explorer/fetchAndUpdateKeys");
|
||||
updateUserContext({
|
||||
@@ -482,6 +490,23 @@ export async function fetchAndUpdateKeys(subscriptionId: string, resourceGroup:
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
if (error.code === "AuthorizationFailed") {
|
||||
keys = await getReadOnlyKeys(subscriptionId, resourceGroup, account);
|
||||
Logger.logInfo(
|
||||
`Read only Keys fetched for ${userContext.apiType} account ${account}`,
|
||||
"Explorer/fetchAndUpdateKeys",
|
||||
);
|
||||
updateUserContext({
|
||||
masterKey: keys.primaryReadonlyMasterKey,
|
||||
});
|
||||
} else {
|
||||
logConsoleError(`Error occurred fetching keys for the account." ${error.message}`);
|
||||
Logger.logError(
|
||||
`Error during fetching keys or updating user context: ${error} for ${userContext.apiType} account ${account}`,
|
||||
"Explorer/fetchAndUpdateKeys",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,6 +600,22 @@ async function configurePortal(): Promise<Explorer> {
|
||||
"Explorer/configurePortal",
|
||||
);
|
||||
await fetchAndUpdateKeys(subscriptionId, resourceGroup, account.name);
|
||||
} else {
|
||||
Logger.logInfo(
|
||||
`Trying to silently acquire MSAL token for ${userContext.apiType} account ${account.name}`,
|
||||
"Explorer/configurePortal",
|
||||
);
|
||||
try {
|
||||
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, true);
|
||||
updateUserContext({ aadToken: aadToken });
|
||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||
} catch (authError) {
|
||||
Logger.logWarning(
|
||||
`Failed to silently acquire authorization token from MSAL: ${authError} for ${userContext.apiType} account ${account}`,
|
||||
"Explorer/configurePortal",
|
||||
);
|
||||
logConsoleError("Failed to silently acquire authorization token: " + authError);
|
||||
}
|
||||
}
|
||||
|
||||
updateUserContext({ dataPlaneRbacEnabled });
|
||||
@@ -672,6 +713,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
databaseAccount,
|
||||
resourceGroup: inputs.resourceGroup,
|
||||
subscriptionId: inputs.subscriptionId,
|
||||
tenantId: inputs.tenantId,
|
||||
subscriptionType: inputs.subscriptionType,
|
||||
quotaId: inputs.quotaId,
|
||||
portalEnv: inputs.serverId as PortalEnv,
|
||||
@@ -792,4 +834,4 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
||||
|
||||
interface SampledataconnectionResponse {
|
||||
connectionString: string;
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,20 @@ export interface SidePanelState {
|
||||
headerText?: string;
|
||||
openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void;
|
||||
closeSidePanel: () => void;
|
||||
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
|
||||
}
|
||||
|
||||
export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
|
||||
isOpen: false,
|
||||
panelWidth: "440px",
|
||||
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>
|
||||
set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })),
|
||||
closeSidePanel: () => set((state) => ({ ...state, isOpen: false })),
|
||||
closeSidePanel: () => {
|
||||
const lastFocusedElement = useSidePanel.getState().getRef;
|
||||
set((state) => ({ ...state, isOpen: false }));
|
||||
const timeoutId = setTimeout(() => {
|
||||
lastFocusedElement?.current?.focus();
|
||||
set({ getRef: undefined });
|
||||
}, 300);
|
||||
return () => clearTimeout(timeoutId);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph
|
||||
import useSWR from "swr";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { Subscription } from "../Contracts/DataModels";
|
||||
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface SubscriptionListResult {
|
||||
@@ -92,3 +93,5 @@ export function useSubscriptions(armToken: string): Subscription[] | undefined {
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ test("Cassandra keyspace and table CRUD", async ({ page }) => {
|
||||
async (panel, okButton) => {
|
||||
await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId);
|
||||
await panel.getByPlaceholder("Enter table Id").fill(tableId);
|
||||
await panel.getByLabel("Table max RU/s").fill("1000");
|
||||
await okButton.click();
|
||||
},
|
||||
{ closeTimeout: 5 * 60 * 1000 },
|
||||
|
||||
14
test/fx.ts
14
test/fx.ts
@@ -40,15 +40,15 @@ export enum TestAccount {
|
||||
}
|
||||
|
||||
export const defaultAccounts: Record<TestAccount, string> = {
|
||||
[TestAccount.Tables]: "portal-tables-runner",
|
||||
[TestAccount.Cassandra]: "portal-cassandra-runner",
|
||||
[TestAccount.Gremlin]: "portal-gremlin-runner",
|
||||
[TestAccount.Mongo]: "portal-mongo-runner",
|
||||
[TestAccount.Mongo32]: "portal-mongo32-runner",
|
||||
[TestAccount.SQL]: "portal-sql-runner-west-us",
|
||||
[TestAccount.Tables]: "github-e2etests-tables",
|
||||
[TestAccount.Cassandra]: "github-e2etests-cassandra",
|
||||
[TestAccount.Gremlin]: "github-e2etests-gremlin",
|
||||
[TestAccount.Mongo]: "github-e2etests-mongo",
|
||||
[TestAccount.Mongo32]: "github-e2etests-mongo32",
|
||||
[TestAccount.SQL]: "github-e2etests-sql",
|
||||
};
|
||||
|
||||
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "runners";
|
||||
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
||||
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c";
|
||||
|
||||
function tryGetStandardName(accountType: TestAccount) {
|
||||
|
||||
@@ -16,7 +16,6 @@ test("Gremlin graph CRUD", async ({ page }) => {
|
||||
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
|
||||
await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId);
|
||||
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
|
||||
await panel.getByLabel("Database max RU/s").fill("1000");
|
||||
await okButton.click();
|
||||
},
|
||||
{ closeTimeout: 5 * 60 * 1000 },
|
||||
|
||||
@@ -21,7 +21,6 @@ import { DataExplorer, TestAccount, generateUniqueName } from "../fx";
|
||||
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
|
||||
await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId);
|
||||
await panel.getByRole("textbox", { name: "Shard key" }).fill("pk");
|
||||
await panel.getByLabel("Database max RU/s").fill("1000");
|
||||
await okButton.click();
|
||||
},
|
||||
{ closeTimeout: 5 * 60 * 1000 },
|
||||
|
||||
@@ -15,7 +15,6 @@ test("SQL database and container CRUD", async ({ page }) => {
|
||||
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
|
||||
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
|
||||
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
|
||||
await panel.getByLabel("Database max RU/s").fill("1000");
|
||||
await okButton.click();
|
||||
},
|
||||
{ closeTimeout: 5 * 60 * 1000 },
|
||||
|
||||
@@ -3,7 +3,7 @@ const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
||||
const ms = require("ms");
|
||||
|
||||
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"];
|
||||
const resourceGroupName = "runners";
|
||||
const resourceGroupName = "de-e2e-tests";
|
||||
|
||||
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
|
||||
const AZURE_CLIENT_ID = "fd8753b0-0707-4e32-84e9-2532af865fb4";
|
||||
const AZURE_TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47";
|
||||
const SUBSCRIPTION_ID = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
|
||||
const RESOURCE_GROUP = "runners";
|
||||
const RESOURCE_GROUP = "de-e2e-tests";
|
||||
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET || process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET; // TODO Remove. Exists for backwards compat with old .env files. Prefer AZURE_CLIENT_SECRET
|
||||
|
||||
if (!AZURE_CLIENT_SECRET) {
|
||||
@@ -301,7 +301,7 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
logLevel: "debug",
|
||||
bypass: (req, res) => {
|
||||
@@ -312,7 +312,7 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
},
|
||||
},
|
||||
"/proxy": {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
|
||||
Reference in New Issue
Block a user