Compare commits

..

66 Commits

Author SHA1 Message Date
Senthamil Sindhu
a6473adf40 Add recent changes 2024-11-19 20:56:54 -08:00
Senthamil Sindhu
451316cad4 Merge branch 'users/sindhuba/listKeys' into users/sindhuba/refresh-token 2024-10-27 20:59:04 -07:00
Senthamil Sindhu
b456e53b2f Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-10 00:16:31 -07:00
sindhuba
ac2e2a6f8e Add tenantId info in Data Explorer while opening from Portal (#1987)
* Fix API endpoint for CassandraProxy query API

* activate Mongo Proxy and Cassandra Proxy in Prod

* Add CP Prod endpoint

* Run npm format and tests

* Revert code

* fix bug that blocked local mongo proxy and cassandra proxy development

* Add prod endpoint

* fix pr check tests

* Remove prod

* Remove prod endpoint

* Remove dev endpoint

* Support data plane RBAC

* Support data plane RBAC

* Add additional changes for Portal RBAC functionality

* Remove unnecessary code

* Remove unnecessary code

* Add code to fix VCoreMongo/PG bug

* Address feedback

* Add more logs for RBAC feature

* Add more logs for RBAC features

* Add AAD endpoints for all environments

* Add AAD endpoints

* Run npm format

* Support multi-tenant switching for Data plane RBAC

* Run npm format

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-10-09 15:41:58 -07:00
Laurent Nguyen
3138580eae Move column selection out of mpac (#1980) 2024-10-09 14:23:31 +02:00
Senthamil Sindhu
de5ba041e9 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-08 14:40:04 -07:00
SATYA SB
aa88815c6e [Keyboard Navigation - Azure Cosmos DB - Data Explorer]: Keyboard focus is not retaining back to 'more' button after closing 'Delete container' dialog. (#1978)
* [accessibility-2262594]: [Keyboard Navigation - Azure Cosmos DB - Data Explorer]: Keyboard focus is not retaining back to 'more' button after closing 'Delete container' dialog.

* Optimize closeSidePanel: add timeout cleanup to prevent memory leaks and ensure proper focus behavior

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-10-07 09:09:23 +05:30
Senthamil Sindhu
ae7184f7ea Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-04 15:50:19 -07:00
Vsevolod Kukol
5a2f78b51e Improve Entra ID token acquisition logic (#1940)
* Add a silent parameter to acquireTokenWithMsal

If true, the function won't retry to sign in using a Popup if silent token acquisition fails.

* Improve Login for Entra ID RBAC button logic

Try to reuse an existing signed-in MSAL account to get the AAD token
and fall back to full sign-in otherwise.

Also move the logic to AuthorizationUtils

* Try to acquire an Entra ID token silently on startup.

When running in Portal MSAL should be able to reuse the
MSAL account from Portal and allow us to silently get
the RBAC token. If it fails we'll show the Login for Entry ID RBAC
button as usual.

* Small code improvements

* Remove the RBAC notice from settings pane
and try to acquire RBAC token silently after enabling RBAC.

* Use msal.ssoSilent with an optional login hint
to avoid more sign-in popups.
msal.loginPopup will be used as a backup option if ssoSilent fails.
Ideally the parent environment (Portal/Fabric) should send
a loginHint with the username of the currently signed in user that
can be passed to the token acquisition flow.

* Improve RBAC error wording, clarifying where to find the Login button.
2024-10-04 08:45:29 +02:00
Senthamil Sindhu
3a6769280b t Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-02 19:56:51 -07:00
bogercraig
fbc2e1335b Pull Additional Allowed Cassandra and Mongo Proxy Endpoints from Deployed Config (#1984)
* Updating to take default cassandra proxy endpoints from external config.json.

* Updating allow list for mongo proxy endpoints.
2024-10-02 14:05:21 -07:00
Senthamil Sindhu
4768ba3642 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-01 19:55:09 -07:00
SATYA SB
eb0d7b71b3 [accessibility-3100029]:[Screen Reader - Azure Cosmos DB - Add Table Row]: Descriptive Label is not provided for 'Value' edit fields under 'Add Table Row' pane. (#1970)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-10-01 09:15:25 +05:30
Asier Isayas
261289b031 Remove legacy backend references in tests and local dev (#1983)
* remove legacy backend references in tests and local dev

* fix unit tests

* fixed bulk delete

* fix tests

* fix cosmosclient

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-09-30 14:34:37 -04:00
Asier Isayas
fae4589427 Bulk Delete API fix (#1977)
* Bulk Delete API fix

* Bulk Delete API fix

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-09-30 11:40:48 -04:00
jawelton74
cbcb7e6240 Switch E2E tests to use new accounts. (#1982) 2024-09-30 07:29:24 -07:00
jawelton74
e0b773d920 Set shared throughput default to false for New Databases (#1981)
* Introduce common function for shared throughput default and set to
false.

* Add new file.

* Adjust E2E tests to not set throughput for database create.
2024-09-27 09:59:41 -07:00
Ashley Stanton-Nurse
9ec2cea95c Ensure the "Ctrl+Alt+["/"Ctrl+Alt+]" shortcuts don't get triggered on "AltGr+8"/"AltGr+9" (#1979)
* Remove the "Ctrl+Alt+[" and "Ctrl+Alt+]" shortcuts, as they conflict on non-US keyboard layouts

* Use "BracketLeft" and "BracketRight" to re-enable shortcut for US keyboards
2024-09-25 09:15:54 -07:00
Ashley Stanton-Nurse
1a4f713a79 Clarifying copy-edit to delete database panel (#1974)
* change 'Database id' to 'Database name' in Delete Database confirm prompt

* put 'name' in a parenthetical instead of replacing 'id'

* update test snapshots
2024-09-23 11:34:49 -07:00
Senthamil Sindhu
bd564c665b Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-09-17 16:12:01 -07:00
Senthamil Sindhu
7e5c6420ad Run npm format 2024-08-21 13:36:46 -07:00
Senthamil Sindhu
89a3a040d8 Add AAD endpoints 2024-08-21 13:31:02 -07:00
Senthamil Sindhu
ec3afa0526 Add AAD endpoints for all environments 2024-08-21 10:11:35 -07:00
Senthamil Sindhu
4176a8a9a9 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-08-21 10:11:04 -07:00
Senthamil Sindhu
311cf9aa5a Add AAD endpoints for all environments 2024-08-21 10:08:55 -07:00
Senthamil Sindhu
bc8094f44f Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-08-05 10:02:29 -07:00
Senthamil Sindhu
9203276a24 Add common code for ARM token refresh 2024-08-05 09:57:54 -07:00
Senthamil Sindhu
d7825f4f78 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-30 08:23:50 -07:00
Senthamil Sindhu
e51c28c634 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-24 12:32:54 -07:00
Senthamil Sindhu
3b86a9477f Add code for arm token refresh 2024-07-23 11:14:19 -07:00
Senthamil Sindhu
521ff39eb0 Resolve conflicts 2024-07-19 07:48:12 -07:00
Senthamil Sindhu
2e2db3c2a9 Merge branch 'users/sindhuba/fix-listKeys' into users/sindhuba/listKeys 2024-07-19 07:37:14 -07:00
Senthamil Sindhu
2b84af60f4 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-19 07:31:11 -07:00
Senthamil Sindhu
40283ff7f1 Add readOnlyKeys call for accounts with Reader role 2024-07-19 07:28:39 -07:00
Senthamil Sindhu
29a1a819c3 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-16 14:28:50 -07:00
Senthamil Sindhu
5a16eec29d Add more logs for RBAC features 2024-07-10 07:38:37 -07:00
Senthamil Sindhu
2b11e0e52b Add more logs for RBAC feature 2024-07-10 07:30:34 -07:00
Senthamil Sindhu
89374a16ba Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-09 21:13:41 -07:00
Senthamil Sindhu
dc289ece75 Address feedback 2024-07-09 07:31:09 -07:00
Senthamil Sindhu
1ddd372c6d Add code to fix VCoreMongo/PG bug 2024-07-08 14:46:11 -07:00
Senthamil Sindhu
2740657b4a Remove unnecessary code 2024-07-08 14:39:49 -07:00
Senthamil Sindhu
8c888a751c Remove unnecessary code 2024-07-08 14:26:04 -07:00
Senthamil Sindhu
8140f0edb1 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-08 13:45:54 -07:00
Senthamil Sindhu
ab5239df09 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-08 13:27:24 -07:00
Senthamil Sindhu
3e48393fbb Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-03 00:07:17 -07:00
Senthamil Sindhu
0079a9147f Resolved merge conflict 2024-07-01 16:22:04 -07:00
Senthamil Sindhu
912688dc14 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-06-27 11:00:31 -07:00
Senthamil Sindhu
8849526fab Merge branch 'add-dp-rbac' of https://github.com/Azure/cosmos-explorer 2024-06-19 15:20:20 -07:00
Senthamil Sindhu
24af64a66d Add additional changes for Portal RBAC functionality 2024-06-19 15:05:14 -07:00
Senthamil Sindhu
be871737ad Support data plane RBAC 2024-06-14 12:45:21 -07:00
Senthamil Sindhu
4d8bb5c3ea Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-06-14 12:18:14 -07:00
Senthamil Sindhu
10a8505b9a Support data plane RBAC 2024-06-14 12:12:30 -07:00
Senthamil Sindhu
ef7c2fe2f7 Remove dev endpoint 2024-04-10 11:59:57 -07:00
Senthamil Sindhu
4c7aca95e1 Merge branch 'users/aisayas/mp-cp-activate-prod' of https://github.com/Azure/cosmos-explorer into users/sindhuba/activate-prod 2024-04-09 12:27:51 -07:00
Senthamil Sindhu
2243ad895a Remove prod endpoint 2024-04-09 12:16:13 -07:00
Senthamil Sindhu
b2d5f91fe1 Remove prod 2024-04-09 11:22:17 -07:00
Asier Isayas
a712193477 fix pr check tests 2024-04-09 11:43:24 -04:00
Senthamil Sindhu
5ee411693c Add prod endpoint 2024-04-09 08:41:47 -07:00
Asier Isayas
16c7b2567b fix bug that blocked local mongo proxy and cassandra proxy development 2024-04-09 11:39:11 -04:00
Senthamil Sindhu
78d9a0cd8d Revert code 2024-04-08 16:20:40 -07:00
Senthamil Sindhu
c6ad538559 Run npm format and tests 2024-04-08 15:58:10 -07:00
Senthamil Sindhu
2bc09a6efe Add CP Prod endpoint 2024-04-08 15:37:19 -07:00
Senthamil Sindhu
d3a3033b25 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-04-08 15:32:50 -07:00
Asier Isayas
6bdc714e11 activate Mongo Proxy and Cassandra Proxy in Prod 2024-04-08 16:52:09 -04:00
Senthamil Sindhu
5042f28229 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-03-25 15:11:53 -07:00
Senthamil Sindhu
e1430fd06f Fix API endpoint for CassandraProxy query API 2024-03-18 10:25:17 -07:00
74 changed files with 1005 additions and 605 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -2618,7 +2618,6 @@ a:link {
.tabPanesContainer {
display: flex;
flex-grow: 1;
flex-direction: column;
overflow: hidden;
}

View File

@@ -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",

View File

@@ -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";

View File

@@ -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 = {};

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
export function getNewDatabaseSharedThroughputDefault(): boolean {
return false;
}

View File

@@ -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}
/>
</>
);
};

View File

@@ -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`);
});
});
});

View File

@@ -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 {

View File

@@ -135,6 +135,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
onEntityValueChange={onEntityValueChange}
onSelectDate={onSelectDate}
onEntityTimeValueChange={onEntityTimeValueChange}
entityProperty={entityProperty}
/>
{!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip">

View File

@@ -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;
}

View File

@@ -381,6 +381,7 @@ export enum TerminalKind {
export interface DataExplorerInputsFrame {
databaseAccount: any;
subscriptionId?: string;
tenantId?: string;
resourceGroup?: string;
masterKey?: string;
hasWriteAccess?: boolean;

View File

@@ -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",

View File

@@ -1,14 +1,23 @@
/**
* React component for Command button component.
*/
import Explorer from "Explorer/Explorer";
import { KeyboardAction } from "KeyboardShortcuts";
import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as StringUtils from "../../../Utils/StringUtils";
/**
* Options for this component
*/
export interface CommandButtonComponentProps {
/**
* font icon name for the button
*/
iconName?: string;
/**
* image source for the button icon
*/
@@ -22,7 +31,7 @@ export interface CommandButtonComponentProps {
/**
* Click handler for command button click
*/
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent, container: Explorer) => void;
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
/**
* Label for the button
@@ -111,3 +120,157 @@ export interface CommandButtonComponentProps {
*/
keyboardAction?: KeyboardAction;
}
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
private dropdownElt: HTMLElement;
private expandButtonElt: HTMLElement;
public componentDidUpdate(): void {
if (!this.dropdownElt || !this.expandButtonElt) {
return;
}
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
}
private onKeyPress(event: React.KeyboardEvent): boolean {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
this.commandClickCallback && this.commandClickCallback(event);
event.stopPropagation();
return false;
}
return true;
}
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
if (event.keyCode === KeyCodes.DownArrow) {
$(this.dropdownElt).hide();
$(this.dropdownElt).show().focus();
event.stopPropagation();
return false;
}
if (event.keyCode === KeyCodes.UpArrow) {
$(this.dropdownElt).hide();
event.stopPropagation();
return false;
}
return true;
}
private getCommandButtonId(): string {
if (this.props.id) {
return this.props.id;
} else {
return `commandButton-${StringUtils.stripSpacesFromString(this.props.commandButtonLabel)}`;
}
}
public static renderButton(options: CommandButtonComponentProps, key?: string): JSX.Element {
return <CommandButtonComponent key={key} {...options} />;
}
private commandClickCallback(e: React.SyntheticEvent): void {
if (this.props.disabled) {
return;
}
// TODO Query component's parent, not document
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
if (el) {
el.style.display = "none";
}
this.props.onCommandClick(e);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
commandButtonClicked: this.props.commandButtonLabel,
});
}
private renderChildren(): JSX.Element {
if (!this.props.children || this.props.children.length < 1) {
return <React.Fragment />;
}
return (
<div
className="commandExpand"
tabIndex={0}
ref={(ref: HTMLElement) => {
this.expandButtonElt = ref;
}}
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
>
<div className="commandDropdownLauncher">
<span className="partialSplitter" />
<span className="expandDropdown">
<img src={CollapseChevronDownIcon} />
</span>
</div>
<div
className="commandDropdownContainer"
ref={(ref: HTMLElement) => {
this.dropdownElt = ref;
}}
>
<div className="commandDropdown">
{this.props.children.map((c: CommandButtonComponentProps, index: number): JSX.Element => {
return CommandButtonComponent.renderButton(c, `${index}`);
})}
</div>
</div>
</div>
);
}
public static renderLabel(
props: CommandButtonComponentProps,
key?: string,
refct?: (input: HTMLElement) => void,
): JSX.Element {
if (!props.commandButtonLabel) {
return <React.Fragment />;
}
return (
<span className="commandLabel" key={key} ref={refct}>
{props.commandButtonLabel}
</span>
);
}
public render(): JSX.Element {
let mainClassName = "commandButtonComponent";
if (this.props.disabled) {
mainClassName += " commandDisabled";
}
if (this.props.isSelected) {
mainClassName += " selectedButton";
}
let contentClassName = "commandContent";
if (this.props.children && this.props.children.length > 0) {
contentClassName += " hasHiddenItems";
}
return (
<div className="commandButtonReact">
<span
className={mainClassName}
role="menuitem"
tabIndex={this.props.tabIndex}
onKeyPress={(e: React.KeyboardEvent<HTMLSpanElement>) => this.onKeyPress(e)}
title={this.props.tooltipText}
id={this.getCommandButtonId()}
aria-disabled={this.props.disabled}
aria-haspopup={this.props.hasPopup}
aria-label={this.props.ariaLabel}
onClick={(e: React.MouseEvent<HTMLSpanElement>) => this.commandClickCallback(e)}
>
<div className={contentClassName}>
<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />
{CommandButtonComponent.renderLabel(this.props)}
</div>
</span>
{this.props.children && this.renderChildren()}
</div>
);
}
}

View File

@@ -7,7 +7,6 @@ import {
ContainerVectorPolicyComponent,
ContainerVectorPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useDatabases } from "Explorer/useDatabases";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
@@ -33,6 +32,7 @@ import {
PartitionKeyComponent,
PartitionKeyComponentProps,
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";

View File

@@ -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[] => {
@@ -107,7 +108,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
const onOpenChange = useCallback(
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
if (data.type === "Click" && node.onClick) {
if (data.type === "Click" && !isBranch && node.onClick) {
node.onClick();
}
if (!node.isExpanded && data.open && node.onExpanded) {
@@ -119,7 +120,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
node.onCollapsed?.();
}
},
[node, setIsLoading],
[isBranch, node, setIsLoading],
);
const onMenuOpenChange = useCallback(
@@ -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>

View File

@@ -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>

View File

@@ -5,13 +5,12 @@ import { Environment, getEnvironment } from "Common/EnvironmentUtility";
import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
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";
@@ -48,6 +47,7 @@ import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
@@ -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) {

View File

@@ -4,51 +4,83 @@
* and update any knockout observables passed from the parent.
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import {
createPlatformButtons,
createStaticCommandBarButtons,
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { Platform, configContext } from "../../../ConfigContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
interface Props {
container: Explorer;
}
export interface CommandBarStore {
contextButtons: CommandButtonComponentProps[];
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
isHidden: boolean;
setIsHidden: (isHidden: boolean) => void;
}
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
contextButtons: [],
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
}));
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const selectedNodeState = useSelectedNode();
const contextButtons = useCommandBar((state) => state.contextButtons);
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight;
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
const platformButtons = createPlatformButtons();
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor, container);
if (contextButtons?.length > 0) {
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
userContext.apiType === "Postgres"
? CommandBarComponentButtonFactory.createPostgreButtons(container)
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
styles={{
root: { backgroundColor: backgroundColor },
}}
overflowButtonProps={{ ariaLabel: "More commands" }}
/>
</div>
);
}
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
const contextButtons = (buttons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
if (buttons && buttons.length > 0) {
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
}
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(
contextButtons || [],
backgroundColor,
container,
);
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
if (uiFabricTabsButtons.length > 0) {
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
}
const uiFabricPlatformButtons = CommandBarUtil.convertButton(platformButtons || [], backgroundColor, container);
uiFabricPlatformButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
const connectionInfo = useNotebook((state) => state.connectionInfo);
@@ -56,7 +88,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
connectionInfo?.status !== ConnectionStatusType.Connect
) {
uiFabricPlatformButtons.unshift(
uiFabricControlButtons.unshift(
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
);
}
@@ -75,8 +107,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
},
};
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons, container);
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
setKeyboardHandlers(keyboardHandlers);
return (
@@ -84,7 +116,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
farItems={uiFabricPlatformButtons}
farItems={uiFabricControlButtons}
styles={rootStyle}
overflowButtonProps={{ ariaLabel: "More commands" }}
/>

View File

@@ -3,12 +3,15 @@ import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer;
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
describe("Enable Azure Synapse Link Button", () => {
@@ -16,6 +19,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
databaseAccount: {
properties: {
@@ -26,7 +30,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Button should be visible", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
);
@@ -42,7 +46,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
);
@@ -58,7 +62,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
);
@@ -71,6 +75,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
databaseAccount: {
properties: {
@@ -103,7 +108,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@@ -113,13 +118,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@@ -129,8 +134,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
const openPostgresShellButtonLabel = "Open PSQL shell";
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
beforeAll(() => {
mockExplorer = {} as Explorer;
});
it("creates Postgres shell button", () => {
const buttons = CommandBarComponentButtonFactory.createPostgreButtons();
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
const openPostgresShellButton = buttons.find(
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
);
@@ -138,7 +147,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("creates vCore Mongo shell button", () => {
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons();
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
const openVCoreMongoShellButton = buttons.find(
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
);
@@ -153,6 +162,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
updateUserContext({
authType: AuthType.ResourceToken,
});
@@ -164,7 +175,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
expect(buttons[0].disabled).toBe(false);

View File

@@ -21,6 +21,7 @@ import { userContext } from "../../../UserContext";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { OpenFullScreen } from "../../OpenFullScreen";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
@@ -31,20 +32,19 @@ import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
let counter = 0;
export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
return userContext.apiType === "Postgres" ? createPostgreButtons() : createVCoreMongoButtons();
}
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState,
): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(selectedNodeState);
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
}
const buttons: CommandButtonComponentProps[] = [];
// Avoid starting with a divider
const addDivider = () => {
if (buttons.length > 0 && !buttons[buttons.length - 1].isDivider) {
if (buttons.length > 0) {
buttons.push(createDivider());
}
};
@@ -54,7 +54,7 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
const addSynapseLink = createOpenSynapseLinkDialogButton();
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
addDivider();
buttons.push(addSynapseLink);
@@ -67,9 +67,9 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
useEffect(() => {
const buttonProps = createLoginForEntraIDButton();
const buttonProps = createLoginForEntraIDButton(container);
setLoginButtonProps(buttonProps);
}, [dataPlaneRbacEnabled, aadTokenUpdated]);
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
if (loginButtonProps) {
addDivider();
@@ -87,8 +87,8 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
}
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
const openQueryBtn = createOpenQueryButton();
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn);
}
@@ -103,7 +103,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
tooltipText: userContext.features.commandBarV2 ? "New..." : label,
ariaLabel: label,
hasPopup: true,
disabled:
@@ -116,12 +115,21 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
}
}
return buttons;
}
export function createContextCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState,
): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: (_, container) => {
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
@@ -133,7 +141,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
ariaLabel: label,
hasPopup: true,
};
addDivider();
buttons.push(newMongoShellBtn);
}
@@ -146,27 +153,25 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
const newCassandraShellButton: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: (_, container) => {
onCommandClick: () => {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
addDivider();
buttons.push(newCassandraShellButton);
}
return buttons;
}
export function createPlatformButtons(): CommandButtonComponentProps[] {
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: (_, container) =>
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",
@@ -202,7 +207,7 @@ export function createPlatformButtons(): CommandButtonComponentProps[] {
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: (_, container) => container.openCESCVAFeedbackBlade(),
onCommandClick: () => container.openCESCVAFeedbackBlade(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
@@ -234,7 +239,7 @@ function areScriptsSupported(): boolean {
);
}
function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
@@ -252,7 +257,7 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
return {
iconSrc: SynapseIcon,
iconAlt: label,
onCommandClick: (_, container) => container.openEnableSynapseLinkDialog(),
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled:
@@ -261,12 +266,12 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
};
}
function createLoginForEntraIDButton(): CommandButtonComponentProps {
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform !== Platform.Portal) {
return undefined;
}
const handleCommandClick: CommandButtonComponentProps["onCommandClick"] = async (_, container) => {
const handleCommandClick = async () => {
await container.openLoginForEntraIDPopUp();
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
};
@@ -393,14 +398,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
return buttons;
}
function createOpenQueryButton(): CommandButtonComponentProps {
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
tooltipText: userContext.features.commandBarV2 ? "Open Query..." : "Open Query",
keyboardAction: KeyboardAction.OPEN_QUERY,
onCommandClick: (_, container) =>
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label,
ariaLabel: label,
@@ -423,7 +427,10 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
};
}
function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind): CommandButtonComponentProps {
function createOpenTerminalButtonByKind(
container: Explorer,
terminalKind: ViewModels.TerminalKind,
): CommandButtonComponentProps {
const terminalFriendlyName = (): string => {
switch (terminalKind) {
case ViewModels.TerminalKind.Cassandra:
@@ -446,7 +453,7 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: (_, container) => {
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(terminalKind);
}
@@ -460,10 +467,11 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
}
function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState,
): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton();
const openQueryBtn = createOpenQueryButton(container);
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
const isResourceTokenCollectionNodeSelected: boolean =
@@ -476,20 +484,20 @@ function createStaticCommandBarButtonsForResourceToken(
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
if (!openQueryBtn.disabled) {
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
}
return [newSqlQueryBtn, openQueryBtn];
}
export function createPostgreButtons(): CommandButtonComponentProps[] {
const openPostgreShellBtn = createOpenTerminalButtonByKind(ViewModels.TerminalKind.Postgres);
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
return [openPostgreShellBtn];
}
export function createVCoreMongoButtons(): CommandButtonComponentProps[] {
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(ViewModels.TerminalKind.VCoreMongo);
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
return [openVCoreMongoTerminalButton];
}

View File

@@ -1,10 +1,8 @@
import { ICommandBarItemProps } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as CommandBarUtil from "./CommandBarUtil";
describe("CommandBarUtil tests", () => {
const mockExplorer = {} as Explorer;
const createButton = (): CommandButtonComponentProps => {
return {
iconSrc: "icon",
@@ -24,7 +22,7 @@ describe("CommandBarUtil tests", () => {
const btn = createButton();
const backgroundColor = "backgroundColor";
const converteds = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer);
const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
expect(converteds.length).toBe(1);
const converted = converteds[0];
expect(converted.split).toBe(undefined);
@@ -48,7 +46,7 @@ describe("CommandBarUtil tests", () => {
btn.children.push(child);
}
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor", mockExplorer);
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
expect(converteds.length).toBe(1);
const converted = converteds[0];
expect(converted.split).toBe(true);
@@ -64,7 +62,7 @@ describe("CommandBarUtil tests", () => {
btns.push(createButton());
}
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor", mockExplorer);
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
const uniqueKeys = converteds
.map((btn: ICommandBarItemProps) => btn.key)
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
@@ -76,10 +74,10 @@ describe("CommandBarUtil tests", () => {
const backgroundColor = "backgroundColor";
btn.commandButtonLabel = undefined;
let converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
expect(converted.text).toEqual(btn.tooltipText);
converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
delete btn.commandButtonLabel;
expect(converted.text).toEqual(btn.tooltipText);
});

View File

@@ -25,11 +25,7 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
* Convert our NavbarButtonConfig to UI Fabric buttons
* @param btns
*/
export const convertButton = (
btns: CommandButtonComponentProps[],
backgroundColor: string,
container: Explorer,
): ICommandBarItemProps[] => {
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx =
configContext.platform == Platform.Fabric
? StyleConstants.FabricCommandBarButtonHeight
@@ -58,14 +54,15 @@ export const convertButton = (
iconProps: {
style: {
width: StyleConstants.CommandBarIconWidth, // 16
alignSelf: undefined,
alignSelf: btn.iconName ? "baseline" : undefined,
filter: getFilter(btn.disabled),
},
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName,
},
onClick: btn.onCommandClick
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev, container);
btn.onCommandClick(ev);
let copilotEnabled = false;
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
@@ -138,7 +135,7 @@ export const convertButton = (
result.split = true;
result.subMenuProps = {
items: convertButton(btn.children, backgroundColor, container),
items: convertButton(btn.children, backgroundColor),
styles: {
list: {
// TODO Figure out how to do it the proper way with subComponentStyles.
@@ -189,7 +186,7 @@ export const convertButton = (
option?: IDropdownOption,
index?: number,
): void => {
btn.children[index].onCommandClick(event, container);
btn.children[index].onCommandClick(event);
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
};
@@ -240,17 +237,14 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
};
};
export function createKeyboardHandlers(
allButtons: CommandButtonComponentProps[],
container: Explorer,
): KeyboardHandlerMap {
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {};
function createHandlers(buttons: CommandButtonComponentProps[]) {
buttons.forEach((button) => {
if (!button.disabled && button.keyboardAction) {
handlers[button.keyboardAction] = (e) => {
button.onCommandClick(e, container);
button.onCommandClick(e);
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
return true;

View File

@@ -1,16 +0,0 @@
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import create, { UseStore } from "zustand";
export interface CommandBarStore {
contextButtons: CommandButtonComponentProps[];
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
isHidden: boolean;
setIsHidden: (isHidden: boolean) => void;
}
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
contextButtons: [],
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
}));

View File

@@ -1,159 +0,0 @@
import {
makeStyles,
Menu,
MenuButton,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
Toolbar,
ToolbarButton,
ToolbarDivider,
ToolbarGroup,
Tooltip,
} from "@fluentui/react-components";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import Explorer from "Explorer/Explorer";
import {
createPlatformButtons,
createStaticCommandBarButtons,
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
import { createKeyboardHandlers } from "Explorer/Menus/CommandBar/CommandBarUtil";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import React, { MouseEventHandler } from "react";
const useToolbarStyles = makeStyles({
toolbar: {
height: tokens.layoutRowHeight,
justifyContent: "space-between", // Ensures that the two toolbar groups are at opposite ends of the toolbar
...cosmosShorthands.borderBottom(),
},
toolbarGroup: {
display: "flex",
},
});
export interface CommandBarV2Props {
explorer: Explorer;
}
export const CommandBarV2: React.FC<CommandBarV2Props> = ({ explorer }: CommandBarV2Props) => {
const styles = useToolbarStyles();
const selectedNodeState = useSelectedNode();
const contextButtons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
const platformButtons = createPlatformButtons();
if (isHidden) {
setKeyboardHandlers({});
return null;
}
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
const keyboardHandlers = createKeyboardHandlers(allButtons, explorer);
setKeyboardHandlers(keyboardHandlers);
return (
<CosmosFluentProvider>
<Toolbar className={styles.toolbar}>
<ToolbarGroup role="presentation" className={styles.toolbarGroup}>
{staticButtons.map((button, index) =>
renderButton(explorer, button, `static-${index}`, contextButtons?.length > 0),
)}
{staticButtons.length > 0 && contextButtons?.length > 0 && <ToolbarDivider />}
{contextButtons.map((button, index) => renderButton(explorer, button, `context-${index}`, false))}
</ToolbarGroup>
<ToolbarGroup role="presentation">
{platformButtons.map((button, index) => renderButton(explorer, button, `platform-${index}`, true))}
</ToolbarGroup>
</Toolbar>
</CosmosFluentProvider>
);
};
// This allows us to migrate individual buttons over to using a JSX.Element for the icon, without requiring us to change them all at once.
function renderIcon(iconSrcOrElement: string | JSX.Element, alt?: string): JSX.Element {
if (typeof iconSrcOrElement === "string") {
return <img src={iconSrcOrElement} alt={alt} />;
}
return iconSrcOrElement;
}
function renderButton(
explorer: Explorer,
btn: CommandButtonComponentProps,
key: string,
iconOnly: boolean,
): JSX.Element {
if (btn.isDivider) {
return <ToolbarDivider key={key} />;
}
const hasChildren = !!btn.children && btn.children.length > 0;
const label = btn.commandButtonLabel || btn.tooltipText;
const tooltip = btn.tooltipText || (iconOnly ? label : undefined);
const onClick: MouseEventHandler | undefined =
btn.onCommandClick && !hasChildren ? (e) => btn.onCommandClick(e, explorer) : undefined;
// We don't know which element will be the top-level element, so just slap a key on all of 'em
let button = hasChildren ? (
<MenuButton key={key} appearance="subtle" aria-label={btn.ariaLabel} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
{!iconOnly && label}
</MenuButton>
) : (
<ToolbarButton key={key} aria-label={btn.ariaLabel} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
{!iconOnly && label}
</ToolbarButton>
);
if (tooltip) {
button = (
<Tooltip key={key} content={tooltip} relationship="description" withArrow>
{button}
</Tooltip>
);
}
if (hasChildren) {
button = (
<Menu key={key}>
<MenuTrigger disableButtonEnhancement>{button}</MenuTrigger>
<MenuPopover>
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
</MenuPopover>
</Menu>
);
}
return button;
}
function renderMenuItem(explorer: Explorer, btn: CommandButtonComponentProps, key: string): JSX.Element {
const hasChildren = !!btn.children && btn.children.length > 0;
const onClick: MouseEventHandler | undefined = btn.onCommandClick
? (e) => btn.onCommandClick(e, explorer)
: undefined;
const item = (
<MenuItem key={key} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
{btn.commandButtonLabel || btn.tooltipText}
</MenuItem>
);
if (hasChildren) {
return (
<Menu>
<MenuTrigger disableButtonEnhancement>{item}</MenuTrigger>
<MenuPopover>
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
</MenuPopover>
</Menu>
);
}
return item;
}

View File

@@ -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."

View File

@@ -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);

View File

@@ -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>
`;

View File

@@ -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"

View File

@@ -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 &quot;Login for Entra ID RBAC&quot; 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.

View File

@@ -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}

View File

@@ -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"

View File

@@ -3,7 +3,7 @@ import { Stack } from "@fluentui/react";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";

View File

@@ -5,7 +5,7 @@ import { Platform, updateConfigContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import {
ButtonsDependencies,
DELETE_BUTTON_ID,
@@ -461,7 +461,7 @@ describe("Documents tab (noSql API)", () => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
});
@@ -471,7 +471,7 @@ describe("Documents tab (noSql API)", () => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
@@ -483,7 +483,7 @@ describe("Documents tab (noSql API)", () => {
await useCommandBar
.getState()
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
expect(useDialog.getState().showOkCancelModalDialog).toHaveBeenCalled();
@@ -494,7 +494,7 @@ describe("Documents tab (noSql API)", () => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
expect(ProgressModalDialog).toHaveBeenCalled();
@@ -508,7 +508,7 @@ describe("Documents tab (noSql API)", () => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
// The implementation uses setTimeout, so wait for it to finish

View File

@@ -28,7 +28,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import {
ColumnsSelection,
@@ -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);
// -------------------------------------------------------

View File

@@ -1,7 +1,7 @@
import { deleteDocuments } from "Common/MongoProxyClient";
import { Platform, updateConfigContext } from "ConfigContext";
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import {
DELETE_BUTTON_ID,
DISCARD_BUTTON_ID,
@@ -163,7 +163,7 @@ describe("Documents tab (Mongo API)", () => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
});
@@ -173,7 +173,7 @@ describe("Documents tab (Mongo API)", () => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
@@ -188,7 +188,7 @@ describe("Documents tab (Mongo API)", () => {
useCommandBar
.getState()
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
.onCommandClick(undefined, undefined);
.onCommandClick(undefined);
});
expect(mockDeleteDocuments).toHaveBeenCalled();

View File

@@ -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>

View File

@@ -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),

View File

@@ -152,6 +152,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
ariaLabel: saveLabel,
children: saveButtonChildren.length && [
{
iconName: "Save",
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
commandButtonLabel: saveLabel,
hasPopup: false,

View File

@@ -6,7 +6,6 @@ import { SplitterDirection } from "Common/Splitter";
import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { monaco } from "Explorer/LazyMonaco";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
@@ -55,6 +54,7 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
import TabsBase from "../TabsBase";

View File

@@ -1,6 +1,5 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { Pivot, PivotItem } from "@fluentui/react";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { KeyboardAction } from "KeyboardShortcuts";
import React from "react";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@@ -16,6 +15,7 @@ import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import StoredProcedure from "../../Tree/StoredProcedure";
import { useSelectedNode } from "../../useSelectedNode";
import ScriptTabBase from "../ScriptTabBase";

View File

@@ -6,8 +6,7 @@ import { IpRule } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { CommandBarV2 } from "Explorer/Menus/CommandBarV2/CommandBarV2";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
@@ -107,7 +106,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
</ul>
</div>
<div className="tabPanesContainer">
{userContext.features.commandBarV2 && <CommandBarV2 explorer={explorer} />}
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
{openedTabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />

View File

@@ -1,4 +1,3 @@
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
@@ -10,6 +9,7 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useTabs } from "../../hooks/useTabs";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { useSelectedNode } from "../useSelectedNode";
// TODO: Use specific actions for logging telemetry data

View File

@@ -1,6 +1,5 @@
import { TriggerDefinition } from "@azure/cosmos";
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { KeyboardAction } from "KeyboardShortcuts";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
@@ -15,6 +14,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import TriggerTab from "./TriggerTab";
const triggerTypeOptions: IDropdownOption[] = [

View File

@@ -1,6 +1,5 @@
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
import { Label, TextField } from "@fluentui/react";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { KeyboardAction } from "KeyboardShortcuts";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
@@ -14,6 +13,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
interface IUserDefinedFunctionTabContentState {

View File

@@ -1,5 +1,4 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import * as ko from "knockout";
@@ -24,6 +23,7 @@ import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { useTabs } from "../../hooks/useTabs";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import ConflictsTab from "../Tabs/ConflictsTab";
import GraphTab from "../Tabs/GraphTab";

View File

@@ -1,5 +1,4 @@
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil";
import { getItemName } from "Utils/APITypeUtils";
@@ -29,6 +28,7 @@ import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFacto
import { useDialog } from "../Controls/Dialog";
import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";

View File

@@ -1,4 +1,4 @@
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import TabsBase from "Explorer/Tabs/TabsBase";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { useTabs } from "hooks/useTabs";

View File

@@ -2,7 +2,7 @@ import { CapabilityNames } from "Common/Constants";
import { Platform, updateConfigContext } from "ConfigContext";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
import TabsBase from "Explorer/Tabs/TabsBase";

View File

@@ -1,6 +1,5 @@
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
import TabsBase from "Explorer/Tabs/TabsBase";
import StoredProcedure from "Explorer/Tree/StoredProcedure";
@@ -18,6 +17,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useNotebook } from "../Notebook/useNotebook";
import { useSelectedNode } from "../useSelectedNode";

View File

@@ -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"],

View File

@@ -20,11 +20,9 @@ import "../externals/jquery.typeahead.min.css";
import "../externals/jquery.typeahead.min.js";
// Image Dependencies
import { Platform } from "ConfigContext";
import { CommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { SidebarContainer } from "Explorer/Sidebar";
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
import { userContext } from "UserContext";
import "allotment/dist/style.css";
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
@@ -50,6 +48,7 @@ import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
@@ -87,7 +86,7 @@ const App: React.FunctionComponent = () => {
<div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */}
{!userContext.features.commandBarV2 && <CommandBar container={explorer} />}
<CommandBar container={explorer} />
{/* Collections Tree and Tabs - Begin */}
<SidebarContainer explorer={explorer} />
{/* Collections Tree and Tabs - End */}

View File

@@ -1,18 +1,22 @@
import * as React from "react";
import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent";
import FeedbackIcon from "../../../../images/Feedback.svg";
const onClick = () => {
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback");
};
export const FeedbackCommandButton: React.FunctionComponent = () => {
return (
<div className="feedbackConnectSettingIcons">
<div className="commandButtonReact">
<a href="#" title="Send feedback" aria-haspopup="dialog" onClick={onClick}>
<img src={FeedbackIcon} alt="Send feedback" />
</a>
</div>
<CommandButtonComponent
id="commandbutton-feedback"
iconSrc={FeedbackIcon}
iconAlt="feeback button"
onCommandClick={() =>
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback")
}
ariaLabel="feeback button"
tooltipText="Send feedback"
hasPopup={true}
disabled={false}
/>
</div>
);
};

View File

@@ -38,7 +38,6 @@ export type Features = {
readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
readonly commandBarV2: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@@ -109,7 +108,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
commandBarV2: "true" === get("commandbarv2"),
};
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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}

View File

@@ -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,
});

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
},
}));

View File

@@ -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;
}

View File

@@ -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 },

View File

@@ -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) {

View File

@@ -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 },

View File

@@ -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 },

View File

@@ -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 },

View File

@@ -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();

View File

@@ -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",