mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-10-13 15:28:05 +01:00
Merge branch 'master' into missing_pk_fix
This commit is contained in:
commit
5e8db9b539
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -83,7 +83,7 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run build:contracts
|
||||
- name: Restore Build Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache
|
||||
key: ${{ runner.os }}-build-cache
|
||||
|
@ -61,6 +61,8 @@
|
||||
|
||||
@GalleryBackgroundColor: #fdfdfd;
|
||||
|
||||
@LinkColor: #2d6da4;
|
||||
|
||||
//Icons
|
||||
@InfoIconColor: #0072c6;
|
||||
@WarningIconColor: #db7500;
|
||||
@ -246,6 +248,10 @@
|
||||
outline: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
.focusedBorder() {
|
||||
border: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
/************************************************************************************************
|
||||
Common Toggle Switch
|
||||
*************************************************************************************************/
|
||||
|
@ -1914,13 +1914,20 @@ input::-webkit-calendar-picker-indicator::after {
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
height: 32px;
|
||||
background-color: #f2f2f2;
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
margin-bottom: -0.5px;
|
||||
|
||||
li {
|
||||
// Override the bootstrap defaults here to align with our layout constants.
|
||||
margin-bottom: 0px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
51
package-lock.json
generated
51
package-lock.json
generated
@ -86,7 +86,7 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.6.2",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
@ -12662,7 +12662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sanitize-html": {
|
||||
@ -21799,6 +21801,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
|
||||
"integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
@ -30243,14 +30257,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
|
||||
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"@types/retry": "0.12.2",
|
||||
"is-network-error": "^1.0.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
@ -35997,6 +36017,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"dev": true,
|
||||
@ -36044,6 +36071,20 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"dev": true,
|
||||
|
@ -81,7 +81,7 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.6.2",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
|
37645
preview/package-lock.json
generated
37645
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -97,6 +97,12 @@ export enum CapacityMode {
|
||||
Serverless = "Serverless",
|
||||
}
|
||||
|
||||
export enum WorkloadType {
|
||||
Learning = "Learning",
|
||||
DevelopmentTesting = "Development/Testing",
|
||||
Production = "Production",
|
||||
None = "None",
|
||||
}
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
@ -119,6 +125,7 @@ export class AfecFeatures {
|
||||
|
||||
export class TagNames {
|
||||
public static defaultExperience: string = "defaultExperience";
|
||||
public static WorkloadType: string = "hidden-workload-type";
|
||||
}
|
||||
|
||||
export class MongoDBAccounts {
|
||||
@ -523,6 +530,10 @@ export class ariaLabelForLearnMoreLink {
|
||||
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
||||
}
|
||||
|
||||
export class FeedbackLabels {
|
||||
public static readonly provideFeedback: string = "Provide feedback";
|
||||
}
|
||||
|
||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
|
||||
|
@ -1,14 +1,15 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||
import { PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
@ -19,7 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||
Logger.logInfo(
|
||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||
@ -42,7 +43,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
if (isFabricMirroredKey()) {
|
||||
switch (requestInfo.resourceType) {
|
||||
case Cosmos.ResourceType.conflicts:
|
||||
case Cosmos.ResourceType.container:
|
||||
@ -54,8 +55,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
// User resource tokens
|
||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||
const resourceTokens = (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(
|
||||
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.resourceTokenInfo.resourceTokensTimestamp,
|
||||
);
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
case Cosmos.ResourceType.none:
|
||||
@ -66,7 +72,9 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||
// (which is a valid token, but won't work for these operations).
|
||||
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
const resourceTokens2 = (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens;
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||
@ -125,10 +133,6 @@ export async function getTokenFromAuthService(
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
||||
}
|
||||
|
||||
try {
|
||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
||||
@ -151,34 +155,6 @@ export async function getTokenFromAuthService(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTokenFromAuthService_ToBeDeprecated(
|
||||
verb: string,
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
try {
|
||||
const host = configContext.BACKEND_ENDPOINT;
|
||||
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-encrypted-auth-token": userContext.accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
verb,
|
||||
resourceType,
|
||||
resourceId,
|
||||
}),
|
||||
});
|
||||
//TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json()
|
||||
const result = JSON.parse(await response.json());
|
||||
return result;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||
enum SDKSupportedCapabilities {
|
||||
None = 0,
|
||||
|
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { WorkloadType } from "Common/Constants";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { DatabaseAccount, Tags } from "Contracts/DataModels";
|
||||
import { updateUserContext } from "UserContext";
|
||||
|
||||
describe("Database Account Utility", () => {
|
||||
describe("Workload Type", () => {
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
it("Workload Type should return Learning", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {
|
||||
"hidden-workload-type": WorkloadType.Learning,
|
||||
} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.Learning);
|
||||
});
|
||||
|
||||
it("Workload Type should return None", () => {
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.None);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,3 +1,5 @@
|
||||
import { TagNames, WorkloadType } from "Common/Constants";
|
||||
import { Tags } from "Contracts/DataModels";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
function isVirtualNetworkFilterEnabled() {
|
||||
@ -15,3 +17,12 @@ function isPrivateEndpointConnectionsEnabled() {
|
||||
export function isPublicInternetAccessAllowed(): boolean {
|
||||
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
|
||||
}
|
||||
|
||||
export function getWorkloadType(): WorkloadType {
|
||||
const tags: Tags = userContext?.databaseAccount?.tags;
|
||||
const workloadType: WorkloadType = tags && (tags[TagNames.WorkloadType] as WorkloadType);
|
||||
if (!workloadType) {
|
||||
return WorkloadType.None;
|
||||
}
|
||||
return workloadType;
|
||||
}
|
||||
|
@ -4,16 +4,8 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@ -196,20 +188,8 @@ describe("MongoProxyClient", () => {
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("deleteDocument", () => {
|
||||
describe("deleteDocuments", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateUserContext({
|
||||
@ -226,9 +206,9 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("builds the correct URL", () => {
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@ -238,9 +218,9 @@ describe("MongoProxyClient", () => {
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@ -275,33 +255,4 @@ describe("MongoProxyClient", () => {
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFeatureEndpointOrDefault", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||
});
|
||||
const features = extractFeatures(params);
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
features: features,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a local endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,20 +1,13 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import {
|
||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
import queryString from "querystring";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
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, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@ -67,10 +60,6 @@ export function queryDocuments(
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||
}
|
||||
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
@ -89,7 +78,7 @@ export function queryDocuments(
|
||||
query,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@ -127,76 +116,11 @@ export function queryDocuments(
|
||||
});
|
||||
}
|
||||
|
||||
function queryDocuments_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
isResourceList: boolean,
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||
rid: collection.rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
||||
? collection.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[CosmosSDKConstants.HttpHeaders.IsQuery]: "true",
|
||||
[CosmosSDKConstants.HttpHeaders.PopulateQueryMetrics]: "true",
|
||||
[CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true",
|
||||
[CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true",
|
||||
[CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true",
|
||||
[HttpHeaders.contentType]: "application/query+json",
|
||||
};
|
||||
|
||||
if (continuationToken) {
|
||||
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
||||
}
|
||||
|
||||
const path = isResourceList ? "/resourcelist" : "";
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}${path}?${queryString.stringify(params)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ query }),
|
||||
headers,
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return {
|
||||
continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation),
|
||||
documents: (await response.json()).Documents as DataModels.DocumentId[],
|
||||
headers: response.headers,
|
||||
};
|
||||
}
|
||||
await errorHandling(response, "querying documents", params);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function readDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@ -217,7 +141,7 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@ -237,61 +161,12 @@ export function readDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function readDocument_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
const path = idComponents.slice(0, 4).join("/");
|
||||
const rid = encodeURIComponent(idComponents[5]);
|
||||
const params = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent(
|
||||
JSON.stringify(documentId.partitionKeyHeader()),
|
||||
),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "reading document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function createDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
partitionKeyProperty: string,
|
||||
documentContent: unknown,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
@ -308,7 +183,7 @@ export function createDocument(
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createDocument`, {
|
||||
@ -328,54 +203,12 @@ export function createDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function createDocument_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
partitionKeyProperty: string,
|
||||
documentContent: unknown,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||
rid: collection.rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(documentContent),
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "creating document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
documentContent: string,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@ -396,7 +229,7 @@ export function updateDocument(
|
||||
: "",
|
||||
documentContent,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@ -417,139 +250,6 @@ export function updateDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDocument_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
documentContent: string,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
const path = idComponents.slice(0, 5).join("/");
|
||||
const rid = encodeURIComponent(idComponents[5]);
|
||||
const params = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "PUT",
|
||||
body: documentContent,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "updating document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) {
|
||||
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
const path = idComponents.slice(0, 5).join("/");
|
||||
const rid = encodeURIComponent(idComponents[5]);
|
||||
const params = {
|
||||
databaseID: databaseId,
|
||||
collectionID: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
resourceID: rid,
|
||||
resourceType: "docs",
|
||||
subscriptionID: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
partitionKey:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return await errorHandling(response, "deleting document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocument_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<void> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
const path = idComponents.slice(0, 5).join("/");
|
||||
const rid = encodeURIComponent(idComponents[5]);
|
||||
const params = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return await errorHandling(response, "deleting document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocuments(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
@ -575,7 +275,7 @@ export function deleteDocuments(
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/bulkdelete`, {
|
||||
@ -599,9 +299,6 @@ export function deleteDocuments(
|
||||
export function createMongoCollectionWithProxy(
|
||||
params: DataModels.CreateCollectionParams,
|
||||
): Promise<DataModels.Collection> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const shardKey: string = params.partitionKey?.paths[0];
|
||||
|
||||
@ -622,7 +319,7 @@ export function createMongoCollectionWithProxy(
|
||||
isSharded: !!shardKey,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createCollection`, {
|
||||
@ -642,70 +339,6 @@ export function createMongoCollectionWithProxy(
|
||||
});
|
||||
}
|
||||
|
||||
export function createMongoCollectionWithProxy_ToBeDeprecated(
|
||||
params: DataModels.CreateCollectionParams,
|
||||
): Promise<DataModels.Collection> {
|
||||
const { databaseAccount } = userContext;
|
||||
const shardKey: string = params.partitionKey?.paths[0];
|
||||
const mongoParams: DataModels.MongoParameters = {
|
||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||
db: params.databaseId,
|
||||
coll: params.collectionId,
|
||||
pk: shardKey,
|
||||
offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput,
|
||||
cd: params.createNewDatabase,
|
||||
st: params.databaseLevelThroughput,
|
||||
is: !!shardKey,
|
||||
rid: "",
|
||||
rtype: "colls",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
isAutoPilot: !!params.autoPilotMaxThroughput,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
|
||||
return window
|
||||
.fetch(
|
||||
`${endpoint}/createCollection?${queryString.stringify(
|
||||
mongoParams as unknown as queryString.ParsedUrlQueryInput,
|
||||
)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "creating collection", mongoParams);
|
||||
});
|
||||
}
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
let endpoint;
|
||||
if (useMongoProxyEndpoint(feature)) {
|
||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
} else {
|
||||
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
||||
...defaultAllowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
];
|
||||
endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
return getEndpoint(endpoint);
|
||||
}
|
||||
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
@ -719,84 +352,6 @@ export function getEndpoint(endpoint: string): string {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||
[MongoProxyApi.ResourceList]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.QueryDocuments]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.ReadDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.UpdateDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.DeleteDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.LegacyMongoShell]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.BulkDelete]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
};
|
||||
|
||||
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
||||
}
|
||||
|
||||
export class ThrottlingError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||
|
||||
export function updateStyles(): void {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
if (isFabric()) {
|
||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
);
|
||||
try {
|
||||
let collection: DataModels.Collection;
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (params.createNewDatabase) {
|
||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
@ -12,7 +13,7 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||
try {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||
await deleteCollectionWithARM(databaseId, collectionId);
|
||||
} else {
|
||||
await client().database(databaseId).container(collectionId).delete();
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { ContainerResponse } from "@azure/cosmos";
|
||||
import { Queries } from "Common/Constants";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
@ -16,15 +17,13 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
userContext.fabricContext &&
|
||||
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||
) {
|
||||
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
||||
const collections: DataModels.Collection[] = [];
|
||||
const promises: Promise<ContainerResponse>[] = [];
|
||||
|
||||
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||
for (const collectionResourceId in (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens) {
|
||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||
const resourceIdObj = collectionResourceId.split("/");
|
||||
const tokenDatabaseId = resourceIdObj[1];
|
||||
@ -56,7 +55,8 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
return await readCollectionsWithARM(databaseId);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@ -11,8 +11,9 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
|
||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||
if (isFabricMirroredKey() || isFabricNative()) {
|
||||
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
||||
// For Fabric Native, it is not supported.
|
||||
console.error("Skiping readDatabaseOffer for Fabric");
|
||||
return undefined;
|
||||
}
|
||||
@ -23,7 +24,8 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
return await readDatabaseOfferWithARM(params.databaseId);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
@ -14,8 +15,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
let databases: DataModels.Database[];
|
||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||
if (
|
||||
isFabricMirroredKey() &&
|
||||
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
|
||||
.resourceTokens
|
||||
) {
|
||||
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.resourceTokenInfo;
|
||||
|
||||
const databaseIdsSet = new Set<string>(); // databaseId
|
||||
|
||||
@ -46,13 +52,28 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
}));
|
||||
clearMessage();
|
||||
return databases;
|
||||
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
|
||||
const databaseId = userContext.fabricContext.databaseName;
|
||||
databases = [
|
||||
{
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: databaseId,
|
||||
collections: [],
|
||||
},
|
||||
];
|
||||
clearMessage();
|
||||
return databases;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
databases = await readDatabasesWithARM();
|
||||
} else {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@ -36,7 +37,8 @@ export async function updateCollection(
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||
} else {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@ -56,7 +57,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
||||
|
||||
try {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||
if (params.collectionId) {
|
||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||
} else if (userContext.apiType === "Tables") {
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
@ -50,10 +49,8 @@ export interface ConfigContext {
|
||||
CATALOG_API_KEY: string;
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
BACKEND_ENDPOINT?: string;
|
||||
PORTAL_BACKEND_ENDPOINT: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT: string;
|
||||
CASSANDRA_PROXY_ENDPOINT: string;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
@ -109,7 +106,6 @@ let configContext: Readonly<ConfigContext> = {
|
||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
@ -152,15 +148,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.BACKEND_ENDPOINT,
|
||||
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||
)
|
||||
) {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.MONGO_PROXY_ENDPOINT,
|
||||
@ -170,10 +157,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
|
@ -6,6 +6,7 @@ export interface ArmEntity {
|
||||
location: string;
|
||||
type: string;
|
||||
kind: string;
|
||||
tags?: Tags;
|
||||
}
|
||||
|
||||
export interface DatabaseAccount extends ArmEntity {
|
||||
@ -670,3 +671,5 @@ export interface FeatureRegistration {
|
||||
state: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Tags = { [key: string]: string };
|
||||
|
@ -4,6 +4,7 @@
|
||||
export enum FabricMessageTypes {
|
||||
GetAuthorizationToken = "GetAuthorizationToken",
|
||||
GetAllResourceTokens = "GetAllResourceTokens",
|
||||
GetAccessToken = "GetAccessToken",
|
||||
Ready = "Ready",
|
||||
}
|
||||
|
||||
|
@ -1,47 +1,9 @@
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { AuthorizationToken } from "./FabricMessageTypes";
|
||||
|
||||
// This is the version of these messages
|
||||
export const FABRIC_RPC_VERSION = "2";
|
||||
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
||||
|
||||
// Fabric to Data Explorer
|
||||
|
||||
// TODO Deprecated. Remove this section once DE is updated
|
||||
export type FabricMessageV1 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
message: {
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
error: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "allResourceTokens";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
};
|
||||
};
|
||||
// -----------------------------
|
||||
|
||||
export type FabricMessageV2 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
@ -69,7 +31,7 @@ export type FabricMessageV2 =
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: FabricDatabaseConnectionInfo | undefined;
|
||||
data: ResourceTokenInfo | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@ -79,17 +41,81 @@ export type FabricMessageV2 =
|
||||
};
|
||||
};
|
||||
|
||||
export type CosmosDBTokenResponse = {
|
||||
token: string;
|
||||
date: string;
|
||||
export type FabricMessageV3 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
version: string;
|
||||
id: string;
|
||||
message: InitializeMessageV3<CosmosDbArtifactType>;
|
||||
}
|
||||
| {
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "allResourceTokens_v2";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: ResourceTokenInfo | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "explorerVisible";
|
||||
message: {
|
||||
visible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "accessToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: { accessToken: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type CosmosDBConnectionInfoResponse = {
|
||||
export enum CosmosDbArtifactType {
|
||||
MIRRORED_KEY = "MIRRORED_KEY",
|
||||
MIRRORED_AAD = "MIRRORED_AAD",
|
||||
NATIVE = "NATIVE",
|
||||
}
|
||||
export interface ArtifactConnectionInfo {
|
||||
[CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string };
|
||||
[CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo;
|
||||
[CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo;
|
||||
}
|
||||
|
||||
export interface AccessTokenConnectionInfo {
|
||||
accessToken: string;
|
||||
databaseName: string;
|
||||
accountEndpoint: string;
|
||||
}
|
||||
|
||||
export interface InitializeMessageV3<T extends CosmosDbArtifactType> {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
isReadOnly: boolean;
|
||||
artifactType: T;
|
||||
artifactConnectionInfo: ArtifactConnectionInfo[T];
|
||||
}
|
||||
export interface CosmosDBConnectionInfoResponse {
|
||||
endpoint: string;
|
||||
databaseId: string;
|
||||
resourceTokens: { [resourceId: string]: string };
|
||||
};
|
||||
resourceTokens: Record<string, string> | undefined;
|
||||
accessToken: string | undefined;
|
||||
isReadOnly: boolean;
|
||||
credentialType: "Key" | "OAuth2" | undefined;
|
||||
}
|
||||
|
||||
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
||||
resourceTokensTimestamp: number;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ export enum MessageTypes {
|
||||
OpenPostgreSQLPasswordReset,
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
DisplayNPSSurvey, // unused
|
||||
OpenVCoreMongoNetworkingBlade,
|
||||
OpenVCoreMongoConnectionStringsBlade,
|
||||
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
||||
|
@ -406,7 +406,6 @@ export interface DataExplorerInputsFrame {
|
||||
csmEndpoint?: string;
|
||||
dnsSuffix?: string;
|
||||
serverId?: string;
|
||||
extensionEndpoint?: string;
|
||||
portalBackendEndpoint?: string;
|
||||
mongoProxyEndpoint?: string;
|
||||
cassandraProxyEndpoint?: string;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@ -19,7 +21,6 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
@ -41,7 +42,7 @@ export interface DatabaseContextMenuButtonParams {
|
||||
* New resource tree (in ReactJS)
|
||||
*/
|
||||
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -53,7 +54,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
@ -145,7 +146,7 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
@ -34,10 +35,23 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
setIsThroughputCapExceeded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
let defaultThroughput: number;
|
||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||
|
||||
if (
|
||||
isFreeTier ||
|
||||
isQuickstart ||
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||
) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
|
||||
} else {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
|
||||
}
|
||||
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
|
||||
);
|
||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
@ -47,7 +61,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
|
||||
useEffect(() => {
|
||||
// throughput cap check for the initial state
|
||||
let totalThroughput = 0;
|
||||
@ -157,9 +170,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
const defaultThroughput = isFreeTier
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(defaultThroughput);
|
||||
|
@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
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 { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||
@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
@ -187,6 +187,10 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (isFabricMirrored()) {
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
@ -347,8 +351,8 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||
if (isFabricMirroredKey()) {
|
||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1127,7 +1131,7 @@ export default class Explorer {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
|
||||
await this.refreshSampleData();
|
||||
this.refreshSampleData();
|
||||
}
|
||||
|
||||
public async configureCopilot(): Promise<void> {
|
||||
@ -1152,26 +1156,27 @@ export default class Explorer {
|
||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||
}
|
||||
|
||||
public async refreshSampleData(): Promise<void> {
|
||||
try {
|
||||
public refreshSampleData(): void {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
return;
|
||||
}
|
||||
const collection: DataModels.Collection = await readSampleCollection();
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer");
|
||||
readSampleCollection()
|
||||
.then((collection: DataModels.Collection) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,6 @@
|
||||
.flex-direction(@direction: row);
|
||||
padding: 4px 5px;
|
||||
|
||||
label {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.valueCol {
|
||||
flex-grow: 1;
|
||||
padding-right: 5px;
|
||||
@ -63,6 +59,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.customTrashIcon {
|
||||
padding-top: 33px;
|
||||
}
|
||||
|
||||
.rightPaneTrashIconImg {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
@ -142,10 +142,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
<div className="labelCol">
|
||||
<TextField
|
||||
className="edgeInput"
|
||||
label={index === 0 && "Key"}
|
||||
type="text"
|
||||
id="propertyKeyNewVertexPane"
|
||||
componentRef={input}
|
||||
aria-required="true"
|
||||
required
|
||||
placeholder="Key"
|
||||
autoComplete="off"
|
||||
aria-label={`Enter value for propery ${index + 1}`}
|
||||
@ -153,11 +154,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||
/>
|
||||
</div>
|
||||
<span className="mandatoryStar">* </span>
|
||||
|
||||
<div className="valueCol">
|
||||
<TextField
|
||||
className="edgeInput"
|
||||
label={index === 0 && "Value"}
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
autoComplete="off"
|
||||
@ -169,6 +170,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
<div>
|
||||
<Dropdown
|
||||
role="combobox"
|
||||
label={index === 0 && "Type"}
|
||||
ariaLabel="Type"
|
||||
placeholder="Select an option"
|
||||
defaultSelectedKey={data.values[0].type}
|
||||
style={{ width: 100 }}
|
||||
@ -181,7 +184,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
</div>
|
||||
<div className="actionCol">
|
||||
<div
|
||||
className="rightPaneTrashIcon rightPaneBtns"
|
||||
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`Delete ${data.key}`}
|
||||
|
@ -6,12 +6,12 @@
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
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";
|
||||
@ -93,8 +93,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const rootStyle =
|
||||
configContext.platform === Platform.Fabric
|
||||
const rootStyle = isFabric()
|
||||
? {
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
|
@ -37,21 +37,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Button should not be visible for Tables API", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
||||
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
||||
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
||||
//
|
||||
// it("Button should not be visible for Tables API", () => {
|
||||
// updateUserContext({
|
||||
// databaseAccount: {
|
||||
// properties: {
|
||||
// capabilities: [{ name: "EnableTable" }],
|
||||
// },
|
||||
// } as DatabaseAccount,
|
||||
// });
|
||||
//
|
||||
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
// const enableAzureSynapseLinkBtn = buttons.find(
|
||||
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||
// );
|
||||
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
//});
|
||||
|
||||
it("Button should not be visible for Cassandra API", () => {
|
||||
updateUserContext({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||
@ -61,7 +62,7 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
}
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||
|
@ -37,6 +37,10 @@
|
||||
background-color:@NotificationHigh;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focusedBorder();
|
||||
}
|
||||
|
||||
.statusBar {
|
||||
.dataTypeIcons {
|
||||
cursor: pointer;
|
||||
|
@ -81,10 +81,6 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
public setElememntRef = (element: HTMLElement): void => {
|
||||
this.consoleHeaderElement = element;
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const numInProgress = this.state.allConsoleData.filter(
|
||||
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
||||
@ -101,7 +97,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
ref={this.setElememntRef}
|
||||
role="button"
|
||||
aria-label="Console"
|
||||
aria-expanded={this.props.isConsoleExpanded}
|
||||
onClick={() => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
@ -109,15 +107,15 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div className="statusBar">
|
||||
<span className="dataTypeIcons">
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={LoadingIcon} alt="in progress items" />
|
||||
<img src={LoadingIcon} alt="In progress items" />
|
||||
<span className="numInProgress">{numInProgress}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={ErrorBlackIcon} alt="error items" />
|
||||
<img src={ErrorBlackIcon} alt="Error items" />
|
||||
<span className="numErroredItems">{numErroredItems}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={infoBubbleIcon} alt="info items" />
|
||||
<img src={infoBubbleIcon} alt="Info items" />
|
||||
<span className="numInfoItems">{numInfoItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
@ -129,17 +127,10 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
||||
<img
|
||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -259,9 +250,6 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
|
||||
private onConsoleWasExpanded = (): void => {
|
||||
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||
this.consoleHeaderElement.focus();
|
||||
}
|
||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||
};
|
||||
|
||||
|
@ -5,10 +5,13 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleContainer"
|
||||
>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
@ -21,7 +24,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
alt="In progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@ -34,7 +37,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
alt="Error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@ -47,7 +50,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
alt="Info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@ -71,15 +74,11 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="ChevronUpIcon"
|
||||
alt="Expand icon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
@ -176,10 +175,13 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleContainer"
|
||||
>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
@ -192,7 +194,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
alt="In progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@ -205,7 +207,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
alt="Error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@ -218,7 +220,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
alt="Info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@ -244,15 +246,11 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="ChevronUpIcon"
|
||||
alt="Expand icon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||
@ -19,7 +19,7 @@ export class NotebookContainerClient {
|
||||
private clearReconnectionAttemptMessage? = () => {};
|
||||
private isResettingWorkspace: boolean;
|
||||
private phoenixClient: PhoenixClient;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private retryOptions: Options;
|
||||
private scheduleTimerId: NodeJS.Timeout;
|
||||
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
@ -128,9 +127,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations)
|
||||
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
|
||||
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
const response = await fetch(disallowedLocationsUri, {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@ -58,9 +58,9 @@ function openCollectionTab(
|
||||
}
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
isFabricMirrored() &&
|
||||
!(
|
||||
// whitelist the tab kinds that are allowed to be opened in Fabric
|
||||
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored
|
||||
(
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@ -284,6 +285,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
|
||||
<div className="panelMainContent">
|
||||
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
||||
<Stack hidden={userContext.apiType === "Tables"}>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
@ -428,6 +430,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
<Separator className="panelSeparator" />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
@ -666,7 +669,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType === "SQL" && (
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
@ -747,7 +750,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
@ -937,7 +940,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
)}
|
||||
{userContext.apiType !== "Tables" && (
|
||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
@ -1260,7 +1263,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
// }
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isServerlessAccount()) {
|
||||
if (isFabricNative() || isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1286,7 +1289,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private shouldShowAnalyticalStoreOptions(): boolean {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -94,6 +94,7 @@
|
||||
padding-left: @MediumSpace;
|
||||
|
||||
.paneErrorLink {
|
||||
color: @LinkColor;
|
||||
cursor: pointer;
|
||||
font-size: @mediumFontSize;
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import {
|
||||
} from "Shared/StorageUtility";
|
||||
import * as StringUtility from "Shared/StringUtility";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||
@ -174,15 +175,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const styles = useStyles();
|
||||
|
||||
const explorerVersion = configContext.gitSha;
|
||||
const isEmulator = configContext.platform === Platform.Emulator;
|
||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||
const showRetrySettings =
|
||||
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") &&
|
||||
!isEmulator;
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const showEnableEntraIdRbac =
|
||||
isDataplaneRbacSupported(userContext.apiType) &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
!isEmulator;
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
|
||||
const shouldShowCopilotSampleDBOption =
|
||||
userContext.apiType === "SQL" &&
|
||||
useQueryCopilot.getState().copilotEnabled &&
|
||||
useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||
!isEmulator;
|
||||
|
||||
const handlerOnSubmit = async () => {
|
||||
setIsExecuting(true);
|
||||
|
||||
@ -541,9 +553,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{userContext.apiType === "SQL" &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric && (
|
||||
{showEnableEntraIdRbac && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
@ -551,8 +561,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||
ID RBAC.
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID
|
||||
RBAC.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
@ -573,7 +583,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{userContext.apiType === "SQL" && (
|
||||
{userContext.apiType === "SQL" && !isEmulator && (
|
||||
<>
|
||||
<AccordionItem value="3">
|
||||
<AccordionHeader>
|
||||
@ -671,7 +681,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionItem>
|
||||
</>
|
||||
)}
|
||||
{(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && (
|
||||
{showRetrySettings && (
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
@ -744,7 +754,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{!isEmulator && (
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
@ -767,6 +777,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<AccordionItem value="8">
|
||||
<AccordionHeader>
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
|
||||
import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import QueryError, { QueryErrorSeverity } from "Common/QueryError";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
@ -393,8 +393,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
},
|
||||
}}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="list"
|
||||
aria-expanded={showSamplePrompts}
|
||||
autoComplete="off"
|
||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||
aria-labelledby="copilot-textfield-label"
|
||||
onRenderSuffix={() => {
|
||||
@ -580,7 +579,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
|
||||
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
|
||||
<Text style={{ fontSize: 12 }}>{FeedbackLabels.provideFeedback}</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
role="status"
|
||||
@ -630,8 +629,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 10 }}
|
||||
aria-label="Like"
|
||||
role="toggle"
|
||||
aria-label={FeedbackLabels.provideFeedback}
|
||||
role="button"
|
||||
title="Like"
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
@ -649,8 +649,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 4px" }}
|
||||
role="toggle"
|
||||
aria-label="Dislike"
|
||||
role="button"
|
||||
aria-label={FeedbackLabels.provideFeedback}
|
||||
title="Dislike"
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
let toggleStatusValue = "Unpressed";
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Areas,
|
||||
BackendApi,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpStatusCodes,
|
||||
@ -32,7 +31,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@ -82,9 +80,7 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
||||
};
|
||||
|
||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
const backendEndpoint: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
|
||||
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
Button,
|
||||
makeStyles,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuButtonProps,
|
||||
@ -7,13 +8,12 @@ import {
|
||||
MenuList,
|
||||
MenuPopover,
|
||||
MenuTrigger,
|
||||
SplitButton,
|
||||
makeStyles,
|
||||
mergeClasses,
|
||||
shorthands,
|
||||
SplitButton,
|
||||
} from "@fluentui/react-components";
|
||||
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { Tabs } from "Explorer/Tabs/Tabs";
|
||||
@ -21,12 +21,13 @@ import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/T
|
||||
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
||||
import { Allotment, AllotmentHandle } from "allotment";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
const useSidebarStyles = makeStyles({
|
||||
sidebar: {
|
||||
@ -109,6 +110,7 @@ interface GlobalCommand {
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
keyboardAction?: KeyboardAction;
|
||||
ref?: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
@ -118,10 +120,11 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
||||
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
||||
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
||||
const primaryFocusableRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const actions = useMemo<GlobalCommand[]>(() => {
|
||||
if (
|
||||
configContext.platform === Platform.Fabric ||
|
||||
(isFabric() && userContext.fabricContext?.isReadOnly) ||
|
||||
userContext.apiType === "Postgres" ||
|
||||
userContext.apiType === "VCoreMongo"
|
||||
) {
|
||||
@ -135,12 +138,15 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
id: "new_collection",
|
||||
label: `New ${getCollectionName()}`,
|
||||
icon: <Add16Regular />,
|
||||
onClick: () => explorer.onNewCollectionClicked(),
|
||||
onClick: () => {
|
||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
||||
explorer.onNewCollectionClicked({ databaseId });
|
||||
},
|
||||
keyboardAction: KeyboardAction.NEW_COLLECTION,
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType !== "Tables") {
|
||||
if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") {
|
||||
actions.push({
|
||||
id: "new_database",
|
||||
label: `New ${getDatabaseName()}`,
|
||||
@ -177,6 +183,16 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
);
|
||||
}, [actions, setKeyboardActions]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (primaryFocusableRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
primaryFocusableRef.current.focus();
|
||||
}, 0);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
if (!primaryAction) {
|
||||
return null;
|
||||
}
|
||||
@ -184,7 +200,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
return (
|
||||
<div className={styles.globalCommandsContainer} data-test="GlobalCommands">
|
||||
{actions.length === 1 ? (
|
||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick}>
|
||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}>
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
) : (
|
||||
@ -194,7 +210,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
<div ref={setGlobalCommandButton}>
|
||||
<SplitButton
|
||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||
primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }}
|
||||
className={styles.globalCommandsSplitButton}
|
||||
icon={primaryAction.icon}
|
||||
>
|
||||
@ -276,7 +292,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
}, [setLoading]);
|
||||
|
||||
const hasGlobalCommands = !(
|
||||
configContext.platform === Platform.Fabric ||
|
||||
isFabricMirrored() ||
|
||||
userContext.apiType === "Postgres" ||
|
||||
userContext.apiType === "VCoreMongo"
|
||||
);
|
||||
|
173
src/Explorer/SplashScreen/FabricHome.tsx
Normal file
173
src/Explorer/SplashScreen/FabricHome.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
export interface SplashScreenProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
homeContainer: {
|
||||
width: "100%",
|
||||
alignContent: "center",
|
||||
},
|
||||
title: {
|
||||
textAlign: "center",
|
||||
fontSize: "20px",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
buttonsContainer: {
|
||||
width: "584px",
|
||||
margin: "auto",
|
||||
display: "grid",
|
||||
padding: "16px",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gap: "10px",
|
||||
gridAutoRows: "minmax(184px, auto)",
|
||||
},
|
||||
one: {
|
||||
gridColumn: "1 / 3",
|
||||
gridRow: "1 / 3",
|
||||
"& svg": {
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
two: {
|
||||
gridColumn: "3",
|
||||
gridRow: "1",
|
||||
"& img": {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
three: {
|
||||
gridColumn: "3",
|
||||
gridRow: "2",
|
||||
"& svg": {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
buttonContainer: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
border: "1px solid #e0e0e0",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
||||
"border-color": tokens.colorNeutralStroke1Hover,
|
||||
},
|
||||
},
|
||||
buttonUpperPart: {
|
||||
textAlign: "center",
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
backgroundColor: "#e3f7ef",
|
||||
},
|
||||
buttonLowerPart: {
|
||||
borderTop: "1px solid #e0e0e0",
|
||||
height: "76px",
|
||||
padding: "8px",
|
||||
"> div:nth-child(1)": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
},
|
||||
footer: {
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
|
||||
interface FabricHomeScreenButtonProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: JSX.Element;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className: string }> = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
||||
// TODO Make this a11y copmliant: aria-label for icon
|
||||
return (
|
||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
||||
<div className={styles.buttonUpperPart}>{icon}</div>
|
||||
<div className={styles.buttonLowerPart}>
|
||||
<div>{title}</div>
|
||||
<div>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
||||
const styles = useStyles();
|
||||
const getSplashScreenButtons = (): JSX.Element => {
|
||||
const buttons: FabricHomeScreenButtonProps[] = [
|
||||
{
|
||||
title: "New container",
|
||||
description: "Create a destination container to store your data",
|
||||
icon: <DocumentAddRegular />,
|
||||
onClick: () => {
|
||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
||||
props.explorer.onNewCollectionClicked({ databaseId });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Sample data",
|
||||
description: "Automatically load sample data in your database",
|
||||
icon: <img src={CosmosDbBlackIcon} />,
|
||||
},
|
||||
{
|
||||
title: "App development",
|
||||
description: "Start here to use an SDK to build your apps",
|
||||
icon: <LinkMultipleRegular />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.buttonsContainer}>
|
||||
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
|
||||
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
|
||||
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const title = "Build your database";
|
||||
return (
|
||||
<div className={styles.homeContainer}>
|
||||
<div className={styles.title} role="heading" aria-label={title}>
|
||||
{title}
|
||||
</div>
|
||||
{getSplashScreenButtons()}
|
||||
<div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
<Link href="https://cosmos.azure.com/docs" target="_blank">
|
||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,7 +3,7 @@ import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs } from "../../Common/Constants";
|
||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
@ -264,9 +264,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
if (!this.useCassandraProxyEndpoint("postQuery")) {
|
||||
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
|
||||
}
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
@ -309,55 +306,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async queryDocuments_ToBeDeprecated(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestQueryApi
|
||||
: Constants.CassandraBackend.queryApi;
|
||||
const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
query,
|
||||
paginationToken,
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
});
|
||||
shouldNotify &&
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`,
|
||||
);
|
||||
return {
|
||||
Results: data.result,
|
||||
ContinuationToken: data.paginationToken,
|
||||
};
|
||||
} catch (error) {
|
||||
shouldNotify &&
|
||||
handleError(
|
||||
error,
|
||||
"QueryDocuments_ToBeDeprecated_Cassandra",
|
||||
`Failed to query rows for table ${collection.id()}`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage?.();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[],
|
||||
@ -471,10 +419,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||
return this.getTableKeys_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
@ -515,52 +459,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`);
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestKeysApi
|
||||
: Constants.CassandraBackend.keysApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKeys>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: CassandraTableKeys) => {
|
||||
collection.cassandraKeys = data;
|
||||
logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`);
|
||||
deferred.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!this.useCassandraProxyEndpoint("getSchema")) {
|
||||
return this.getTableSchema_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
@ -602,52 +501,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
|
||||
const { databaseAccount, authType } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestSchemaApi
|
||||
: Constants.CassandraBackend.schemaApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKey[]>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
collection.cassandraSchema = data.columns;
|
||||
logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`);
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
||||
if (!this.useCassandraProxyEndpoint("createOrDelete")) {
|
||||
return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query);
|
||||
}
|
||||
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
@ -677,38 +531,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery_ToBeDeprecated(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
query: string,
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||
: Constants.CassandraBackend.createOrDeleteApi;
|
||||
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||
resourceId: resourceId,
|
||||
query: query,
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
}).then(
|
||||
(data: any) => {
|
||||
deferred.resolve();
|
||||
},
|
||||
(reason) => {
|
||||
deferred.reject(reason);
|
||||
},
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
||||
if (!cassandraEndpoint) {
|
||||
return cassandraEndpoint;
|
||||
@ -747,23 +569,4 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||
return collection.cassandraKeys.partitionKeys[0].property;
|
||||
}
|
||||
|
||||
private useCassandraProxyEndpoint(api: string): boolean {
|
||||
const activeCassandraProxyEndpoints: string[] = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
CassandraProxyEndpoints.Fairfax,
|
||||
CassandraProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
if (configContext.globallyEnabledCassandraAPIs.includes(api)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
||||
import { Platform, updateConfigContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||
@ -341,10 +342,15 @@ describe("Documents tab (noSql API)", () => {
|
||||
updateConfigContext({ platform: Platform.Fabric });
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
databaseName: "database",
|
||||
artifactInfo: {
|
||||
connectionId: "test",
|
||||
databaseConnectionInfo: undefined,
|
||||
resourceTokenInfo: undefined,
|
||||
},
|
||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
||||
isReadOnly: true,
|
||||
isVisible: true,
|
||||
fabricClientRpcVersion: "rpcVersion",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -20,7 +20,6 @@ import {
|
||||
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||
import { readDocument } from "Common/dataAccess/readDocument";
|
||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
@ -43,6 +42,7 @@ import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@ -344,7 +344,7 @@ export const getTabsButtons = ({
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
}: ButtonsDependencies): CommandButtonComponentProps[] => {
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
// All the following buttons require write access
|
||||
return [];
|
||||
}
|
||||
@ -1155,16 +1155,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
} else {
|
||||
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
||||
}
|
||||
} else {
|
||||
if (isMongoBulkDeleteDisabled) {
|
||||
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument().
|
||||
// MongoProxyClient.deleteDocuments() should be called for all users.
|
||||
deletePromise = MongoProxyClient.deleteDocument(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
toDeleteDocumentIds[0],
|
||||
).then(() => [toDeleteDocumentIds[0]]);
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
} else {
|
||||
deletePromise = MongoProxyClient.deleteDocuments(
|
||||
_collection.databaseId,
|
||||
@ -1177,7 +1167,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return deletePromise
|
||||
.then(
|
||||
@ -2060,11 +2049,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
||||
|
||||
// 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(Constants.MongoProxyApi.BulkDelete);
|
||||
const isBulkDeleteDisabled =
|
||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||
const isBulkDeleteDisabled = partitionKey.systemKey && !isPreferredApiMongoDB;
|
||||
// -------------------------------------------------------
|
||||
|
||||
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
||||
@ -2156,8 +2142,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
selectedColumnIds={selectedColumnIds}
|
||||
columnDefinitions={columnDefinitions}
|
||||
isRowSelectionDisabled={
|
||||
isBulkDeleteDisabled ||
|
||||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||
isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
|
||||
}
|
||||
onColumnSelectionChange={onColumnSelectionChange}
|
||||
defaultColumnSelection={getInitialColumnSelection()}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
|
||||
import React, { Component } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
@ -50,15 +48,13 @@ export default class MongoShellTabComponent extends Component<
|
||||
IMongoShellTabComponentStates
|
||||
> {
|
||||
private _logTraces: Map<string, number>;
|
||||
private _useMongoProxyEndpoint: boolean;
|
||||
|
||||
constructor(props: IMongoShellTabComponentProps) {
|
||||
super(props);
|
||||
this._logTraces = new Map();
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
||||
|
||||
this.state = {
|
||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||
url: getMongoShellUrl(),
|
||||
};
|
||||
|
||||
props.onMongoShellTabAccessor({
|
||||
@ -113,17 +109,9 @@ export default class MongoShellTabComponent extends Component<
|
||||
const resourceId = databaseAccount?.id;
|
||||
const accountName = databaseAccount?.name;
|
||||
const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint;
|
||||
const mongoEndpoint =
|
||||
documentEndpoint.substr(
|
||||
Constants.MongoDBAccounts.protocol.length + 3,
|
||||
documentEndpoint.length -
|
||||
(Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length),
|
||||
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||
const databaseId = this.props.collection.databaseId;
|
||||
const collectionId = this.props.collection.id();
|
||||
const apiEndpoint = this._useMongoProxyEndpoint
|
||||
? configContext.MONGO_PROXY_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
const apiEndpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
const encryptedAuthToken: string = userContext.accessToken;
|
||||
|
||||
shellIframe.contentWindow.postMessage(
|
||||
@ -132,7 +120,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
data: {
|
||||
resourceId: resourceId,
|
||||
accountName: accountName,
|
||||
mongoEndpoint: this._useMongoProxyEndpoint ? documentEndpoint : mongoEndpoint,
|
||||
mongoEndpoint: documentEndpoint,
|
||||
authorization: authorization,
|
||||
databaseId: databaseId,
|
||||
collectionId: collectionId,
|
||||
|
@ -2,8 +2,6 @@ import { Platform, resetConfigContext, updateConfigContext } from "../../../Conf
|
||||
import { updateUserContext, userContext } from "../../../UserContext";
|
||||
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||
|
||||
const mongoBackendEndpoint = "https://localhost:1234";
|
||||
|
||||
describe("getMongoShellUrl", () => {
|
||||
let queryString = "";
|
||||
|
||||
@ -11,7 +9,6 @@ describe("getMongoShellUrl", () => {
|
||||
resetConfigContext();
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||
platform: Platform.Hosted,
|
||||
});
|
||||
|
||||
@ -37,12 +34,7 @@ describe("getMongoShellUrl", () => {
|
||||
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
||||
});
|
||||
|
||||
it("should return /indexv2.html by default", () => {
|
||||
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /index.html when useMongoProxyEndpoint is true", () => {
|
||||
const useMongoProxyEndpoint: boolean = true;
|
||||
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
|
||||
it("should return /index.html by default", () => {
|
||||
expect(getMongoShellUrl().toString()).toContain(`/index.html?${queryString}`);
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { userContext } from "../../../UserContext";
|
||||
|
||||
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
|
||||
export function getMongoShellUrl(): string {
|
||||
const { databaseAccount: account } = userContext;
|
||||
const resourceId = account?.id;
|
||||
const accountName = account?.name;
|
||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||
|
||||
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
|
||||
return `/mongoshell/index.html?${queryString}`;
|
||||
}
|
||||
|
@ -1,21 +1,17 @@
|
||||
import { IMessageBarStyles, MessageBar, MessageBarType } from "@fluentui/react";
|
||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { IpRule } from "Contracts/DataModels";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||
import { FabricHomeScreen } from "Explorer/SplashScreen/FabricHome";
|
||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import ko from "knockout";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
@ -34,10 +30,6 @@ interface TabsProps {
|
||||
|
||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||
const [
|
||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
||||
|
||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
|
||||
useEffect(() => {
|
||||
@ -48,28 +40,8 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
});
|
||||
}, [setKeyboardHandlers]);
|
||||
|
||||
const defaultMessageBarStyles: IMessageBarStyles = {
|
||||
root: {
|
||||
height: `${LayoutConstants.rowHeight}px`,
|
||||
overflow: "hidden",
|
||||
flexDirection: "row",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
styles={defaultMessageBarStyles}
|
||||
onDismiss={() => {
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||
}}
|
||||
>
|
||||
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
||||
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{openedReactTabs.map((tab) => (
|
||||
@ -301,7 +273,11 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
<ConnectTab />
|
||||
);
|
||||
case ReactTabKind.Home:
|
||||
if (isFabricNative()) {
|
||||
return <FabricHomeScreen explorer={explorer} />;
|
||||
} else {
|
||||
return <SplashScreen explorer={explorer} />;
|
||||
}
|
||||
case ReactTabKind.Quickstart:
|
||||
return userContext.apiType === "VCoreMongo" ? (
|
||||
<VcoreMongoQuickstartTab explorer={explorer} />
|
||||
@ -314,57 +290,3 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||
}
|
||||
};
|
||||
|
||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||
if (
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||
(userContext.apiType === "Cassandra" &&
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||
ipRules?.length
|
||||
) {
|
||||
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
||||
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
||||
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||
ipAddressesFromIPRules.includes(legacyPortalBackendIP),
|
||||
);
|
||||
if (!ipRulesIncludeLegacyPortalBackend) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userContext.apiType === "Mongo") {
|
||||
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
||||
configContext.MONGO_PROXY_ENDPOINT,
|
||||
);
|
||||
|
||||
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
|
||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
|
||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||
);
|
||||
|
||||
return !ipRulesIncludeMongoProxy;
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
|
||||
|
||||
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
|
||||
? [
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
|
||||
]
|
||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every(
|
||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||
);
|
||||
|
||||
return !ipRulesIncludeCassandraProxy;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
import VcoreFirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@ -42,7 +43,11 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
return (
|
||||
<QuickstartFirewallNotification
|
||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||
screenshot={FirewallRuleScreenshot}
|
||||
screenshot={
|
||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
||||
? VcoreFirewallRuleScreenshot
|
||||
: FirewallRuleScreenshot
|
||||
}
|
||||
shellName={this.getShellNameForDisplay(this.kind)}
|
||||
/>
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@ -34,7 +35,6 @@ import QueryTablesTab from "../Tabs/QueryTablesTab";
|
||||
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import { Platform, configContext } from "./../../ConfigContext";
|
||||
import ConflictId from "./ConflictId";
|
||||
import DocumentId from "./DocumentId";
|
||||
import StoredProcedure from "./StoredProcedure";
|
||||
@ -210,7 +210,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
|
||||
const showScriptsMenus: boolean =
|
||||
configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||
!isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
|
||||
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
|
||||
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
||||
import { Home16Regular } from "@fluentui/react-icons";
|
||||
import { AuthType } from "AuthType";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
||||
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import {
|
||||
@ -11,6 +10,7 @@ import {
|
||||
} from "Explorer/Tree/treeNodeUtil";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@ -76,8 +76,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
||||
: [];
|
||||
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
|
||||
|
||||
const headerNodes: TreeNode[] =
|
||||
configContext.platform === Platform.Fabric
|
||||
const headerNodes: TreeNode[] = isFabricMirrored()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
|
@ -740,7 +740,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = `
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = `
|
||||
[
|
||||
{
|
||||
"children": [
|
||||
@ -753,6 +753,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@ -774,6 +780,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@ -822,6 +834,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@ -870,6 +888,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@ -915,6 +939,145 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only 1`] = `
|
||||
[
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "standardCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "conflictsCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
],
|
||||
"className": "databaseNode",
|
||||
"contextMenu": undefined,
|
||||
"iconSrc": <DatabaseRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "standardDb",
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "sampleItemsCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
],
|
||||
"className": "databaseNode",
|
||||
"contextMenu": undefined,
|
||||
"iconSrc": <DatabaseRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "sharedDatabase",
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "schemaCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"className": "loadMoreNode",
|
||||
"label": "load more",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"className": "databaseNode",
|
||||
"contextMenu": undefined,
|
||||
"iconSrc": <DatabaseRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "giganticDatabase",
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = `
|
||||
[
|
||||
{
|
||||
@ -972,7 +1135,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
},
|
||||
],
|
||||
"isSelected": [Function],
|
||||
"label": "mockSproc3",
|
||||
"label": "mockSproc4",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
@ -990,7 +1153,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
},
|
||||
],
|
||||
"isSelected": [Function],
|
||||
"label": "mockUdf3",
|
||||
"label": "mockUdf4",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
@ -1008,7 +1171,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
},
|
||||
],
|
||||
"isSelected": [Function],
|
||||
"label": "mockTrigger3",
|
||||
"label": "mockTrigger4",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CapabilityNames } from "Common/Constants";
|
||||
import { Platform, updateConfigContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@ -16,7 +17,7 @@ import {
|
||||
} from "Explorer/Tree/treeNodeUtil";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { updateUserContext } from "UserContext";
|
||||
import { FabricContext, updateUserContext, UserContext } from "UserContext";
|
||||
import PromiseSource from "Utils/PromiseSource";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@ -360,9 +361,30 @@ describe("createDatabaseTreeNodes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>]>([
|
||||
["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }],
|
||||
["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }],
|
||||
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
|
||||
[
|
||||
"the SQL API, on Fabric read-only",
|
||||
Platform.Fabric,
|
||||
false,
|
||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||
{ fabricContext: { isReadOnly: true } as FabricContext<CosmosDbArtifactType> },
|
||||
],
|
||||
[
|
||||
"the SQL API, on Fabric non read-only",
|
||||
Platform.Fabric,
|
||||
false,
|
||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||
{ fabricContext: { isReadOnly: false } as FabricContext<CosmosDbArtifactType> },
|
||||
],
|
||||
[
|
||||
"the SQL API, on Portal",
|
||||
Platform.Portal,
|
||||
false,
|
||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||
{
|
||||
fabricContext: undefined,
|
||||
},
|
||||
],
|
||||
[
|
||||
"the Cassandra API, serverless, on Hosted",
|
||||
Platform.Hosted,
|
||||
@ -373,6 +395,7 @@ describe("createDatabaseTreeNodes", () => {
|
||||
{ name: CapabilityNames.EnableServerless, description: "" },
|
||||
],
|
||||
},
|
||||
{ fabricContext: undefined },
|
||||
],
|
||||
[
|
||||
"the Mongo API, with Notebooks and Phoenix features, on Emulator",
|
||||
@ -381,11 +404,15 @@ describe("createDatabaseTreeNodes", () => {
|
||||
{
|
||||
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
|
||||
},
|
||||
{ fabricContext: undefined },
|
||||
],
|
||||
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => {
|
||||
])(
|
||||
"generates the correct tree structure for %s",
|
||||
(_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
|
||||
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
|
||||
updateConfigContext({ platform });
|
||||
updateUserContext({
|
||||
...userContext,
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
enableMultipleWriteLocations: true,
|
||||
@ -400,7 +427,8 @@ describe("createDatabaseTreeNodes", () => {
|
||||
refreshActiveTab,
|
||||
);
|
||||
expect(nodes).toMatchSnapshot();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes.
|
||||
// They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle.
|
||||
@ -551,7 +579,18 @@ describe("createDatabaseTreeNodes", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })],
|
||||
[
|
||||
"in Fabric",
|
||||
() => {
|
||||
updateConfigContext({ platform: Platform.Fabric });
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
||||
isReadOnly: true,
|
||||
} as FabricContext<CosmosDbArtifactType>,
|
||||
});
|
||||
},
|
||||
],
|
||||
[
|
||||
"for Cassandra API",
|
||||
() =>
|
||||
|
@ -6,6 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
||||
import Trigger from "Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@ -22,9 +23,7 @@ import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
export const shouldShowScriptNodes = (): boolean => {
|
||||
return (
|
||||
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
|
||||
);
|
||||
return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||
};
|
||||
|
||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||
|
@ -4,7 +4,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { userContext } from "UserContext";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
@ -35,21 +35,26 @@ import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
export class PhoenixClient {
|
||||
private armResourceId: string;
|
||||
private containerHealthHandler: NodeJS.Timeout;
|
||||
private retryOptions: promiseRetry.Options = {
|
||||
private retryOptions: Options = {
|
||||
retries: Notebook.retryAttempts,
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
private abortController: AbortController;
|
||||
private abortSignal: AbortSignal;
|
||||
|
||||
constructor(armResourceId: string) {
|
||||
this.armResourceId = armResourceId;
|
||||
}
|
||||
|
||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||
this.initializeCancelEventListener();
|
||||
|
||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
||||
retries: 4,
|
||||
maxTimeout: 20000,
|
||||
minTimeout: 20000,
|
||||
signal: this.abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
@ -270,6 +275,17 @@ export class PhoenixClient {
|
||||
};
|
||||
}
|
||||
|
||||
private initializeCancelEventListener(): void {
|
||||
this.abortController = new AbortController();
|
||||
this.abortSignal = this.abortController.signal;
|
||||
|
||||
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey && (event.key === "c" || event.key === "z")) {
|
||||
this.abortController.abort(new AbortError("Request canceled"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
||||
const errInfo = jsonData;
|
||||
switch (errInfo?.type) {
|
||||
|
@ -1,56 +1,112 @@
|
||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
||||
import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
|
||||
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
|
||||
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
|
||||
let lastRequestTimestamp: number = undefined;
|
||||
let lastRequestTimestamp: number | undefined = undefined;
|
||||
|
||||
const requestDatabaseResourceTokens = async (): Promise<void> => {
|
||||
/**
|
||||
* Request fabric token:
|
||||
* - Mirrored key and AAD: Database Resource Tokens
|
||||
* - Native: AAD token
|
||||
* @returns
|
||||
*/
|
||||
const requestFabricToken = async (): Promise<void> => {
|
||||
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastRequestTimestamp = Date.now();
|
||||
try {
|
||||
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
|
||||
FabricMessageTypes.GetAllResourceTokens,
|
||||
[],
|
||||
userContext.fabricContext.connectionId,
|
||||
);
|
||||
|
||||
if (!userContext.databaseAccount.properties.documentEndpoint) {
|
||||
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
|
||||
if (isFabricMirrored()) {
|
||||
await requestAndStoreDatabaseResourceTokens();
|
||||
} else if (isFabricNative()) {
|
||||
await requestAndStoreAccessToken();
|
||||
}
|
||||
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
databaseConnectionInfo: fabricDatabaseConnectionInfo,
|
||||
isReadOnly: true,
|
||||
},
|
||||
databaseAccount: { ...userContext.databaseAccount },
|
||||
});
|
||||
scheduleRefreshDatabaseResourceToken();
|
||||
scheduleRefreshFabricToken();
|
||||
} catch (error) {
|
||||
logConsoleError(error);
|
||||
logConsoleError(error as string);
|
||||
throw error;
|
||||
} finally {
|
||||
lastRequestTimestamp = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const requestAndStoreDatabaseResourceTokens = async (): Promise<void> => {
|
||||
if (!userContext.fabricContext || !userContext.databaseAccount) {
|
||||
// This should not happen
|
||||
logConsoleError("Fabric context or database account is missing: cannot request tokens");
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceTokenInfo = await sendCachedDataMessage<ResourceTokenInfo>(
|
||||
FabricMessageTypes.GetAllResourceTokens,
|
||||
[],
|
||||
userContext.fabricContext.artifactInfo?.connectionId,
|
||||
);
|
||||
|
||||
if (!userContext.databaseAccount.properties.documentEndpoint) {
|
||||
userContext.databaseAccount.properties.documentEndpoint = resourceTokenInfo.endpoint;
|
||||
}
|
||||
|
||||
if (resourceTokenInfo.credentialType === "OAuth2") {
|
||||
// Mirrored AAD
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
databaseName: resourceTokenInfo.databaseId,
|
||||
artifactInfo: undefined,
|
||||
isReadOnly: resourceTokenInfo.isReadOnly ?? userContext.fabricContext.isReadOnly,
|
||||
},
|
||||
databaseAccount: { ...userContext.databaseAccount },
|
||||
aadToken: resourceTokenInfo.accessToken,
|
||||
});
|
||||
} else {
|
||||
// TODO: In Fabric contract V2, credentialType is undefined. For V3, it is "Key". Check for "Key" when V3 is supported for Fabric Mirroring Key
|
||||
// Mirrored key
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
databaseName: resourceTokenInfo.databaseId,
|
||||
artifactInfo: {
|
||||
...(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]),
|
||||
resourceTokenInfo,
|
||||
},
|
||||
isReadOnly: resourceTokenInfo.isReadOnly ?? userContext.fabricContext.isReadOnly,
|
||||
},
|
||||
databaseAccount: { ...userContext.databaseAccount },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const requestAndStoreAccessToken = async (): Promise<void> => {
|
||||
if (!userContext.fabricContext || !userContext.databaseAccount) {
|
||||
// This should not happen
|
||||
logConsoleError("Fabric context or database account is missing: cannot request tokens");
|
||||
return;
|
||||
}
|
||||
|
||||
const accessTokenInfo = await sendCachedDataMessage<{ accessToken: string }>(FabricMessageTypes.GetAccessToken, []);
|
||||
|
||||
updateUserContext({
|
||||
aadToken: accessTokenInfo.accessToken,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check token validity and schedule a refresh if necessary
|
||||
* @param tokenTimestamp
|
||||
* @returns
|
||||
*/
|
||||
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
|
||||
export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
@ -59,7 +115,7 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
|
||||
|
||||
timeoutId = setTimeout(
|
||||
() => {
|
||||
requestDatabaseResourceTokens().then(resolve);
|
||||
requestFabricToken().then(resolve);
|
||||
},
|
||||
refreshNow ? 0 : TOKEN_VALIDITY_MS,
|
||||
);
|
||||
@ -68,6 +124,15 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
|
||||
|
||||
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
|
||||
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
|
||||
scheduleRefreshDatabaseResourceToken(true);
|
||||
scheduleRefreshFabricToken(true);
|
||||
}
|
||||
};
|
||||
|
||||
export const isFabric = (): boolean => configContext.platform === Platform.Fabric;
|
||||
export const isFabricMirroredKey = (): boolean =>
|
||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_KEY;
|
||||
export const isFabricMirroredAAD = (): boolean =>
|
||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_AAD;
|
||||
export const isFabricMirrored = (): boolean => isFabricMirroredKey() || isFabricMirroredAAD();
|
||||
export const isFabricNative = (): boolean =>
|
||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.NATIVE;
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { BackendApi, HttpHeaders } from "../../../Common/Constants";
|
||||
import { HttpHeaders } from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
|
||||
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
||||
|
||||
interface Props {
|
||||
@ -19,10 +17,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
|
||||
@ -35,28 +29,10 @@ export const fetchEncryptedToken = async (connectionString: string): Promise<str
|
||||
return decodeURIComponent(encryptedTokenResponse);
|
||||
};
|
||||
|
||||
export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: string): Promise<string> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
// This API has a quirk where it must be parsed twice
|
||||
const result: GenerateTokenResponse = JSON.parse(await response.json());
|
||||
return decodeURIComponent(result.readWrite || result.read);
|
||||
};
|
||||
|
||||
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.AccountRestrictions)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
|
||||
const url = backendEndpoint + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
|
@ -41,13 +41,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
case SelfServeType.example: {
|
||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||
const selfServeExample = new SelfServeExample.default();
|
||||
await loadTranslations(selfServeExample.constructor.name);
|
||||
await loadTranslations(selfServeType);
|
||||
return selfServeExample.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.sqlx: {
|
||||
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||
const sqlX = new SqlX.default();
|
||||
await loadTranslations(sqlX.constructor.name);
|
||||
await loadTranslations(selfServeType);
|
||||
return sqlX.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.graphapicompute: {
|
||||
@ -55,7 +55,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||
);
|
||||
const graphAPICompute = new GraphAPICompute.default();
|
||||
await loadTranslations(graphAPICompute.constructor.name);
|
||||
await loadTranslations(selfServeType);
|
||||
return graphAPICompute.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.materializedviewsbuilder: {
|
||||
@ -63,7 +63,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||
);
|
||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||
await loadTranslations(materializedViewsBuilder.constructor.name);
|
||||
await loadTranslations(selfServeType);
|
||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||
}
|
||||
default:
|
||||
@ -103,7 +103,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType;
|
||||
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType];
|
||||
const selfServeType = SelfServeType[selfServeTypeText.toLocaleLowerCase() as keyof typeof SelfServeType];
|
||||
if (
|
||||
!inputs.subscriptionId ||
|
||||
!inputs.resourceGroup ||
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { TFunction } from "i18next";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import React from "react";
|
||||
import { WithTranslation } from "react-i18next";
|
||||
import * as _ from "underscore";
|
||||
@ -80,7 +80,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
private static readonly defaultRetryIntervalInMs = 30000;
|
||||
private smartUiGeneratorClassName: string;
|
||||
private retryIntervalInMs: number;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private retryOptions: Options;
|
||||
private translationFunction: TFunction;
|
||||
|
||||
componentDidMount(): void {
|
||||
|
@ -29,10 +29,11 @@ export enum SelfServeType {
|
||||
// Unsupported self serve type passed as feature flag
|
||||
invalid = "invalid",
|
||||
// Add your self serve types here
|
||||
example = "example",
|
||||
sqlx = "sqlx",
|
||||
graphapicompute = "graphapicompute",
|
||||
materializedviewsbuilder = "materializedviewsbuilder",
|
||||
// NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\
|
||||
example = "SelfServeExample",
|
||||
sqlx = "SqlX",
|
||||
graphapicompute = "GraphAPICompute",
|
||||
materializedviewsbuilder = "MaterializedViewsBuilder",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -197,6 +197,11 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let billingCurrency;
|
||||
for (const region of map.keys()) {
|
||||
// if no offering id is found for that region, skipping calling price API
|
||||
const subMap = map.get(region);
|
||||
if (!subMap || subMap.size === 0) {
|
||||
continue;
|
||||
}
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
const regionShortName = await getRegionShortName(region);
|
||||
const requestBody: OfferingIdRequest = {
|
||||
@ -237,7 +242,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||
return { priceMap: undefined, billingCurrency: undefined };
|
||||
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
@ -286,6 +291,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||
return undefined;
|
||||
return new Map<string, Map<string, string>>();
|
||||
}
|
||||
};
|
||||
|
@ -227,11 +227,13 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
let costPerHour = 0;
|
||||
let costBreakdown = "";
|
||||
for (const regionItem of regions) {
|
||||
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
|
||||
const incrementalCost = priceMap?.get(regionItem.locationName)?.get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||
} else if (incrementalCost === 0) {
|
||||
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
||||
} else if (currencyCode === undefined) {
|
||||
throw new Error(`Currency code not found in price map.`);
|
||||
}
|
||||
|
||||
let regionalInstanceCount = instanceCount;
|
||||
|
@ -17,7 +17,7 @@ export class JupyterLabAppFactory {
|
||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
||||
this.restartShell = true;
|
||||
}
|
||||
return content?.includes("cosmosuser@");
|
||||
return content?.includes("cosmosshelluser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
@ -68,7 +68,6 @@ export class JupyterLabAppFactory {
|
||||
const session = await manager.startNew();
|
||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||
const content = message.content && message.content[0]?.toString();
|
||||
|
||||
if (this.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
@ -114,6 +113,13 @@ export class JupyterLabAppFactory {
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
// Close terminal when Ctrl key is pressed
|
||||
term.node.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey) {
|
||||
this.onShellExited(false);
|
||||
}
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
@ -47,11 +47,21 @@ export interface VCoreMongoConnectionParams {
|
||||
connectionString: string;
|
||||
}
|
||||
|
||||
interface FabricContext {
|
||||
export interface FabricArtifactInfo {
|
||||
[CosmosDbArtifactType.MIRRORED_KEY]: {
|
||||
connectionId: string;
|
||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||
resourceTokenInfo: ResourceTokenInfo | undefined;
|
||||
};
|
||||
[CosmosDbArtifactType.MIRRORED_AAD]: undefined;
|
||||
[CosmosDbArtifactType.NATIVE]: undefined;
|
||||
}
|
||||
export interface FabricContext<T extends CosmosDbArtifactType> {
|
||||
fabricClientRpcVersion: string;
|
||||
isReadOnly: boolean;
|
||||
isVisible: boolean;
|
||||
databaseName: string;
|
||||
artifactType: CosmosDbArtifactType;
|
||||
artifactInfo: FabricArtifactInfo[T];
|
||||
}
|
||||
|
||||
export type AdminFeedbackControlPolicy =
|
||||
@ -70,7 +80,7 @@ export type AdminFeedbackPolicySettings = {
|
||||
};
|
||||
|
||||
export interface UserContext {
|
||||
readonly fabricContext?: FabricContext;
|
||||
readonly fabricContext?: FabricContext<CosmosDbArtifactType>;
|
||||
readonly authType?: AuthType;
|
||||
readonly masterKey?: string;
|
||||
readonly subscriptionId?: string;
|
||||
|
@ -89,3 +89,7 @@ export const getItemName = (): string => {
|
||||
return "Items";
|
||||
}
|
||||
};
|
||||
|
||||
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
||||
return apiType === "SQL" || apiType === "Tables";
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
export const autoPilotThroughput1K = 1000;
|
||||
export const autoPilotIncrementStep = 1000;
|
||||
export const autoPilotThroughput4K = 4000;
|
||||
export const autoPilotThroughput10K = 10000;
|
||||
|
||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||
if (!maxThroughput) {
|
||||
|
@ -1,11 +1,4 @@
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
@ -73,9 +66,6 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
||||
//"https://main2.documentdb.ext.azure.com": ["104.42.196.69"],
|
||||
"https://main.documentdb.ext.azure.cn": ["139.217.8.252"],
|
||||
"https://main.documentdb.ext.azure.us": ["52.244.48.71"],
|
||||
// Add ussec and usnat when endpoint address is known:
|
||||
//ussec: ["29.26.26.67", "29.26.26.66"],
|
||||
//usnat: ["7.28.202.68"],
|
||||
};
|
||||
|
||||
export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
|
||||
@ -100,14 +90,6 @@ export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://main.cosmos.ext.azure",
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
@ -153,53 +135,3 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
|
||||
//
|
||||
// Temporary function to determine if a portal backend API is supported by the
|
||||
// new backend in this environment.
|
||||
//
|
||||
// TODO: Remove this function once new backend migration is completed for all environments.
|
||||
//
|
||||
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
||||
// This maps backend APIs to the environments supported by the new backend.
|
||||
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
|
||||
[BackendApi.GenerateToken]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.PortalSettings]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.AccountRestrictions]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.RuntimeProxy]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.DisallowedLocations]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
PortalBackendEndpoints.Fairfax,
|
||||
PortalBackendEndpoints.Mooncake,
|
||||
],
|
||||
[BackendApi.SampleData]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
};
|
||||
|
||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
|
||||
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
|
||||
@ -7,7 +8,7 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
|
||||
if (currentWindow.parent === currentWindow) {
|
||||
return undefined;
|
||||
}
|
||||
if (configContext.platform === Platform.Fabric && currentWindow.parent.parent === currentWindow.top) {
|
||||
if (isFabric() && currentWindow.parent.parent === currentWindow.top) {
|
||||
// in Fabric data explorer is inside an extension iframe, so we have two parent iframes
|
||||
return currentWindow;
|
||||
}
|
||||
|
@ -2,18 +2,26 @@ import * as Constants from "Common/Constants";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
|
||||
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
|
||||
import {
|
||||
ArtifactConnectionInfo,
|
||||
CosmosDbArtifactType,
|
||||
FABRIC_RPC_VERSION,
|
||||
FabricMessageV2,
|
||||
FabricMessageV3,
|
||||
InitializeMessageV3,
|
||||
} from "Contracts/FabricMessagesContract";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
||||
import {
|
||||
AppStateComponentNames,
|
||||
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||
readSubComponentState,
|
||||
} from "Shared/AppStatePersistenceUtility";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@ -23,7 +31,7 @@ import { AccountKind, Flights } from "../Common/Constants";
|
||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
||||
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
|
||||
import { configContext, Platform, updateConfigContext } from "../ConfigContext";
|
||||
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
|
||||
@ -44,7 +52,7 @@ import {
|
||||
} from "../Platform/Hosted/HostedUtils";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||
import { FabricArtifactInfo, Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||
import {
|
||||
acquireMsalTokenForAccount,
|
||||
acquireTokenWithMsal,
|
||||
@ -104,7 +112,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
|
||||
async function configureFabric(): Promise<Explorer> {
|
||||
// These are the versions of Fabric that Data Explorer supports.
|
||||
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
|
||||
const SUPPORTED_FABRIC_VERSIONS = ["2", FABRIC_RPC_VERSION];
|
||||
|
||||
let firstContainerOpened = false;
|
||||
let explorer: Explorer;
|
||||
@ -120,7 +128,7 @@ async function configureFabric(): Promise<Explorer> {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: FabricMessageV2 = event.data?.data;
|
||||
const data: FabricMessageV2 | FabricMessageV3 = event.data?.data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
@ -129,38 +137,77 @@ async function configureFabric(): Promise<Explorer> {
|
||||
case "initialize": {
|
||||
const fabricVersion = data.version;
|
||||
if (!SUPPORTED_FABRIC_VERSIONS.includes(fabricVersion)) {
|
||||
// TODO Surface error to user
|
||||
// TODO Surface error to user and log to telemetry
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog("Unsupported Fabric version", `Unsupported Fabric version: ${fabricVersion}`);
|
||||
Logger.logError(`Unsupported Fabric version: ${fabricVersion}`, "Explorer/configureFabric");
|
||||
console.error(`Unsupported Fabric version: ${fabricVersion}`);
|
||||
return;
|
||||
}
|
||||
|
||||
explorer = createExplorerFabric(data.message);
|
||||
await scheduleRefreshDatabaseResourceToken(true);
|
||||
if (fabricVersion === "2") {
|
||||
// ----------------- TODO: Remove this when FabricMessageV2 is deprecated -----------------
|
||||
const initializationMessage = data.message as {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
};
|
||||
|
||||
explorer = createExplorerFabricLegacy(initializationMessage, data.version);
|
||||
await scheduleRefreshFabricToken(true);
|
||||
resolve(explorer);
|
||||
await explorer.refreshAllDatabases();
|
||||
if (userContext.fabricContext.isVisible) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseName);
|
||||
}
|
||||
// -----------------------------------------------------------------------------------------
|
||||
} else if (fabricVersion === FABRIC_RPC_VERSION) {
|
||||
const initializationMessage = data.message as InitializeMessageV3<CosmosDbArtifactType>;
|
||||
explorer = createExplorerFabric(initializationMessage, data.version);
|
||||
|
||||
if (initializationMessage.artifactType === CosmosDbArtifactType.MIRRORED_KEY) {
|
||||
// Do not show Home tab for Mirrored
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
||||
}
|
||||
|
||||
// All tokens used in fabric expire, so schedule a refresh
|
||||
// For Mirrored key, we need the token right away to get the database and containers list.
|
||||
if (isFabricMirroredKey()) {
|
||||
await scheduleRefreshFabricToken(true);
|
||||
} else {
|
||||
scheduleRefreshFabricToken(false);
|
||||
}
|
||||
|
||||
resolve(explorer);
|
||||
await explorer.refreshAllDatabases();
|
||||
|
||||
const { databaseName } = userContext.fabricContext;
|
||||
if (userContext.fabricContext.isVisible && databaseName) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, databaseName);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "newContainer":
|
||||
explorer.onNewCollectionClicked();
|
||||
break;
|
||||
case "authorizationToken":
|
||||
case "allResourceTokens_v2": {
|
||||
case "allResourceTokens_v2":
|
||||
case "accessToken": {
|
||||
handleCachedDataMessage(data);
|
||||
break;
|
||||
}
|
||||
case "explorerVisible": {
|
||||
userContext.fabricContext.isVisible = data.message.visible;
|
||||
if (
|
||||
userContext.fabricContext.isVisible &&
|
||||
!firstContainerOpened &&
|
||||
userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined
|
||||
) {
|
||||
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
|
||||
const { databaseName } = userContext.fabricContext;
|
||||
if (databaseName !== undefined) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
openFirstContainer(explorer, databaseName);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -300,7 +347,7 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
);
|
||||
if (!userContext.features.enableAadDataPlane) {
|
||||
Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD");
|
||||
if (userContext.apiType === "SQL") {
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||
Logger.logInfo(
|
||||
@ -420,13 +467,29 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
||||
return explorer;
|
||||
}
|
||||
|
||||
function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer {
|
||||
/**
|
||||
* Initialization for FabricMessageV2
|
||||
* TODO: delete when FabricMessageV2 is deprecated
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
function createExplorerFabricLegacy(
|
||||
params: { connectionId: string; isVisible: boolean },
|
||||
fabricClientRpcVersion: string,
|
||||
): Explorer {
|
||||
const artifactInfo: FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY] = {
|
||||
connectionId: params.connectionId,
|
||||
resourceTokenInfo: undefined,
|
||||
};
|
||||
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
connectionId: params.connectionId,
|
||||
databaseConnectionInfo: undefined,
|
||||
fabricClientRpcVersion,
|
||||
isReadOnly: true,
|
||||
isVisible: params.isVisible ?? true,
|
||||
databaseName: undefined,
|
||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
||||
artifactInfo,
|
||||
},
|
||||
authType: AuthType.ConnectionString,
|
||||
databaseAccount: {
|
||||
@ -440,11 +503,102 @@ function createExplorerFabric(params: { connectionId: string; isVisible: boolean
|
||||
},
|
||||
},
|
||||
});
|
||||
useTabs.getState().closeAllTabs();
|
||||
const explorer = new Explorer();
|
||||
return explorer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization for FabricMessageV3 and above
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
const createExplorerFabric = (
|
||||
params: InitializeMessageV3<CosmosDbArtifactType>,
|
||||
fabricClientRpcVersion: string,
|
||||
): Explorer => {
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
fabricClientRpcVersion,
|
||||
databaseName: undefined,
|
||||
isVisible: params.isVisible,
|
||||
isReadOnly: params.isReadOnly,
|
||||
artifactType: params.artifactType,
|
||||
artifactInfo: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (params.artifactType === CosmosDbArtifactType.MIRRORED_KEY) {
|
||||
updateUserContext({
|
||||
authType: AuthType.ConnectionString, // TODO: will need its own type
|
||||
databaseAccount: {
|
||||
id: "",
|
||||
location: "",
|
||||
type: "",
|
||||
name: "Mounted", // TODO: not used?
|
||||
kind: AccountKind.Default,
|
||||
properties: {
|
||||
documentEndpoint: undefined,
|
||||
},
|
||||
},
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
artifactInfo: {
|
||||
connectionId: (params.artifactConnectionInfo as ArtifactConnectionInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.connectionId,
|
||||
resourceTokenInfo: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (params.artifactType === CosmosDbArtifactType.MIRRORED_AAD) {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
id: "",
|
||||
location: "",
|
||||
type: "",
|
||||
name: "Mounted", // TODO: not used?
|
||||
kind: AccountKind.Default,
|
||||
properties: {
|
||||
documentEndpoint: undefined,
|
||||
},
|
||||
},
|
||||
authType: AuthType.AAD,
|
||||
dataPlaneRbacEnabled: true,
|
||||
aadToken: undefined,
|
||||
masterKey: undefined,
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
artifactInfo: undefined,
|
||||
},
|
||||
});
|
||||
} else if (params.artifactType === CosmosDbArtifactType.NATIVE) {
|
||||
const nativeParams = params as InitializeMessageV3<CosmosDbArtifactType.NATIVE>;
|
||||
// Make it behave like Hosted/AAD/RBAC
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
id: "",
|
||||
location: "",
|
||||
type: "",
|
||||
name: "Native", // TODO: not used?
|
||||
kind: AccountKind.Default,
|
||||
properties: {
|
||||
documentEndpoint: nativeParams.artifactConnectionInfo.accountEndpoint,
|
||||
},
|
||||
},
|
||||
authType: AuthType.AAD,
|
||||
dataPlaneRbacEnabled: true,
|
||||
aadToken: nativeParams.artifactConnectionInfo.accessToken,
|
||||
masterKey: undefined,
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
databaseName: nativeParams.artifactConnectionInfo.databaseName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const explorer = new Explorer();
|
||||
return explorer;
|
||||
};
|
||||
|
||||
function configureWithEncryptedToken(config: EncryptedToken): Explorer {
|
||||
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
|
||||
updateUserContext({
|
||||
@ -548,20 +702,12 @@ async function configurePortal(): Promise<Explorer> {
|
||||
const inputs = message?.inputs;
|
||||
const openAction = message?.openAction;
|
||||
if (inputs) {
|
||||
if (
|
||||
configContext.BACKEND_ENDPOINT &&
|
||||
configContext.platform === Platform.Portal &&
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
}
|
||||
|
||||
updateContextsFromPortalMessage(inputs);
|
||||
|
||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||
|
||||
let dataPlaneRbacEnabled;
|
||||
if (userContext.apiType === "SQL") {
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||
const isDataPlaneRbacSetting = LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled);
|
||||
Logger.logInfo(
|
||||
@ -680,18 +826,17 @@ function updateAADEndpoints(portalEnv: PortalEnv) {
|
||||
|
||||
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (
|
||||
configContext.BACKEND_ENDPOINT &&
|
||||
configContext.PORTAL_BACKEND_ENDPOINT &&
|
||||
configContext.platform === Platform.Portal &&
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
inputs.portalBackendEndpoint = configContext.PROXY_PATH;
|
||||
}
|
||||
|
||||
const authorizationToken = inputs.authorizationToken || "";
|
||||
const databaseAccount = inputs.databaseAccount;
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
||||
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
|
||||
@ -794,17 +939,7 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
let url: string;
|
||||
if (useNewPortalBackendEndpoint(Constants.BackendApi.SampleData)) {
|
||||
url = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
||||
} else {
|
||||
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
|
||||
? `/api/tokens/sampledataconnection/v2`
|
||||
: `/api/tokens/sampledataconnection`;
|
||||
|
||||
url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
|
||||
}
|
||||
|
||||
const url: string = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
@ -820,7 +955,7 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
||||
const sampleDataConnectionInfo = parseResourceTokenConnectionString(data.connectionString);
|
||||
updateUserContext({ sampleDataConnectionInfo });
|
||||
|
||||
await explorer.refreshSampleData();
|
||||
explorer.refreshSampleData();
|
||||
}
|
||||
|
||||
interface SampledataconnectionResponse {
|
||||
|
@ -1,16 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { ApiEndpoints, BackendApi, HttpHeaders } from "../Common/Constants";
|
||||
import { HttpHeaders } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { AccessInputMetadata } from "../Contracts/DataModels";
|
||||
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
|
||||
|
||||
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||
return fetchAccessData_ToBeDeprecated(portalToken);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
||||
@ -25,25 +18,6 @@ export async function fetchAccessData(portalToken: string): Promise<AccessInputM
|
||||
.catch((error) => console.error(error));
|
||||
}
|
||||
|
||||
export async function fetchAccessData_ToBeDeprecated(portalToken: string): Promise<AccessInputMetadata> {
|
||||
const headers = new Headers();
|
||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
return (
|
||||
fetch(url, options)
|
||||
.then((response) => response.json())
|
||||
// Portal encrypted token API quirk: The response is double JSON encoded
|
||||
.then((json) => JSON.parse(json))
|
||||
.catch((error) => console.error(error))
|
||||
);
|
||||
}
|
||||
|
||||
export function useTokenMetadata(token: string): AccessInputMetadata | undefined {
|
||||
const [state, setState] = useState<AccessInputMetadata | undefined>();
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { clamp } from "@fluentui/react";
|
||||
import { OpenTab } from "Contracts/ActionContracts";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import {
|
||||
AppStateComponentNames,
|
||||
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||
@ -11,7 +12,6 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
|
||||
import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
|
||||
export interface TabsState {
|
||||
openedTabs: TabsBase[];
|
||||
@ -51,22 +51,11 @@ export enum ReactTabKind {
|
||||
QueryCopilot,
|
||||
}
|
||||
|
||||
// HACK: using this const when the configuration context is not initialized yet.
|
||||
// Since Fabric is always setting the url param, use that instead of the regular config.
|
||||
const isPlatformFabric = (() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.has("platform")) {
|
||||
const platform = params.get("platform");
|
||||
return platform === Platform.Fabric;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
openedTabs: [],
|
||||
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
|
||||
activeTab: undefined,
|
||||
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
|
||||
openedTabs: [] as TabsBase[],
|
||||
openedReactTabs: [ReactTabKind.Home],
|
||||
activeTab: undefined as TabsBase,
|
||||
activeReactTab: ReactTabKind.Home,
|
||||
queryCopilotTabInitialInput: "",
|
||||
isTabExecuting: false,
|
||||
isQueryErrorThrown: false,
|
||||
@ -122,7 +111,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
|
||||
if (updatedTabs.length === 0 && !isFabricMirrored()) {
|
||||
set({ activeTab: undefined, activeReactTab: undefined });
|
||||
}
|
||||
|
||||
@ -162,7 +151,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
}
|
||||
});
|
||||
|
||||
if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
|
||||
if (get().openedTabs.length === 0 && !isFabricMirrored()) {
|
||||
set({ activeTab: undefined, activeReactTab: undefined });
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ const initTestExplorer = async (): Promise<void> => {
|
||||
csmEndpoint: "https://management.azure.com",
|
||||
dnsSuffix: "documents.azure.com",
|
||||
serverId: "prod1",
|
||||
extensionEndpoint: "/proxy",
|
||||
portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com",
|
||||
cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com",
|
||||
|
@ -1,4 +1,4 @@
|
||||
const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
|
||||
const { AzureCliCredential } = require("@azure/identity");
|
||||
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
||||
const ms = require("ms");
|
||||
|
||||
@ -16,7 +16,7 @@ function friendlyTime(date) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const credentials = await msRestNodeAuth.AzureCliCredentials.create();
|
||||
const credentials = new AzureCliCredential();
|
||||
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
||||
for (const account of accounts) {
|
||||
|
@ -248,7 +248,8 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
// These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory
|
||||
compress: false,
|
||||
// Update 2/11/2025: we are removing this flag as our bundles sizes grew so that it can remove dead and unreachable code with compromise of build time
|
||||
// compress: false,
|
||||
mangle: {
|
||||
keep_fnames: true,
|
||||
keep_classnames: true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user