Compare commits

..

1 Commits

Author SHA1 Message Date
asier-isayas
b730e2c6fd Set default RU throughput for Production workload accounts to be 10k (#2070) (#2072)
* assign default throughput based on workload type

* combined common logic

* fix unit tests

* add tests

* update tests

* npm run format

* Set default RU throughput for Production workload accounts to be 10k

* remove unused method

* refactor

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-03-11 13:57:05 -04:00
67 changed files with 1641 additions and 37490 deletions

2
.gitignore vendored
View File

@@ -21,5 +21,3 @@ GettingStarted-ignore*.ipynb
/playwright-report/
/blob-report/
/playwright/.cache/
/.vs/cosmos-explorer
/.vs/slnx.sqlite-journal

Binary file not shown.

View File

@@ -248,10 +248,6 @@
outline: 1px dashed @FocusColor;
}
.focusedBorder() {
border: 1px dashed @FocusColor;
}
/************************************************************************************************
Common Toggle Switch
*************************************************************************************************/

View File

@@ -1914,20 +1914,13 @@ 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
View File

@@ -86,7 +86,7 @@
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
"p-retry": "6.2.1",
"p-retry": "4.6.2",
"patch-package": "8.0.0",
"plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",
@@ -12662,9 +12662,7 @@
}
},
"node_modules/@types/retry": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
"version": "0.12.0",
"license": "MIT"
},
"node_modules/@types/sanitize-html": {
@@ -21801,18 +21799,6 @@
"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",
@@ -30257,20 +30243,14 @@
}
},
"node_modules/p-retry": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
"version": "4.6.2",
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.2",
"is-network-error": "^1.0.0",
"@types/retry": "0.12.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=8"
}
},
"node_modules/p-try": {
@@ -36017,13 +35997,6 @@
}
}
},
"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,
@@ -36071,20 +36044,6 @@
"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,

View File

@@ -81,7 +81,7 @@
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
"p-retry": "6.2.1",
"p-retry": "4.6.2",
"patch-package": "8.0.0",
"plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",

37051
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -530,10 +530,6 @@ 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";

View File

@@ -1,14 +1,13 @@
import * as Cosmos from "@azure/cosmos";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { AuthType } from "../AuthType";
import { PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext";
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
import { updateUserContext, userContext } from "../UserContext";
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
@@ -43,7 +42,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return decodeURIComponent(headers.authorization);
}
if (isFabricMirroredKey()) {
if (configContext.platform === Platform.Fabric) {
switch (requestInfo.resourceType) {
case Cosmos.ResourceType.conflicts:
case Cosmos.ResourceType.container:
@@ -55,13 +54,8 @@ 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.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
).resourceTokenInfo.resourceTokens;
checkDatabaseResourceTokensValidity(
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
.resourceTokenInfo.resourceTokensTimestamp,
);
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
case Cosmos.ResourceType.none:
@@ -72,9 +66,7 @@ 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.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
).resourceTokenInfo.resourceTokens;
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
/* ************** TODO: Uncomment this code if we need to support these operations **************

View File

@@ -1,10 +1,10 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { Platform, configContext } from "../ConfigContext";
// 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 (isFabric()) {
if (configContext.platform === Platform.Fabric) {
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;

View File

@@ -1,5 +1,4 @@
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";
@@ -25,7 +24,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
);
try {
let collection: DataModels.Collection;
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput,

View File

@@ -1,4 +1,3 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
@@ -13,7 +12,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 && !isFabric()) {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
await deleteCollectionWithARM(databaseId, collectionId);
} else {
await client().database(databaseId).container(collectionId).delete();

View File

@@ -1,10 +1,9 @@
import { ContainerResponse } from "@azure/cosmos";
import { Queries } from "Common/Constants";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { Platform, configContext } from "ConfigContext";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { FabricArtifactInfo, userContext } from "../../UserContext";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@@ -17,13 +16,15 @@ import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
if (
configContext.platform === Platform.Fabric &&
userContext.fabricContext &&
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
) {
const collections: DataModels.Collection[] = [];
const promises: Promise<ContainerResponse>[] = [];
for (const collectionResourceId in (
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
).resourceTokenInfo.resourceTokens) {
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
// Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1];
@@ -55,8 +56,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if (
userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" &&
!isFabric()
userContext.apiType !== "Tables"
) {
return await readCollectionsWithARM(databaseId);
}

View File

@@ -1,4 +1,4 @@
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Platform, configContext } from "ConfigContext";
import { AuthType } from "../../AuthType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
@@ -11,9 +11,8 @@ import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
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.
if (configContext.platform === Platform.Fabric) {
// TODO This works, but is very slow, because it requests the token, so we skip for now
console.error("Skiping readDatabaseOffer for Fabric");
return undefined;
}
@@ -24,8 +23,7 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
if (
userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" &&
!isFabric()
userContext.apiType !== "Tables"
) {
return await readDatabaseOfferWithARM(params.databaseId);
}

View File

@@ -1,8 +1,7 @@
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Platform, configContext } from "ConfigContext";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { FabricArtifactInfo, userContext } from "../../UserContext";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@@ -15,13 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`);
if (
isFabricMirroredKey() &&
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
.resourceTokens
) {
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
.resourceTokenInfo;
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
const tokensData = userContext.fabricContext.databaseConnectionInfo;
const databaseIdsSet = new Set<string>(); // databaseId
@@ -52,28 +46,13 @@ 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" &&
!isFabric()
userContext.apiType !== "Tables"
) {
databases = await readDatabasesWithARM();
} else {

View File

@@ -1,5 +1,4 @@
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";
@@ -37,8 +36,7 @@ export async function updateCollection(
if (
userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" &&
!isFabric()
userContext.apiType !== "Tables"
) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else {

View File

@@ -1,5 +1,4 @@
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";
@@ -57,7 +56,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 && !isFabric()) {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.apiType === "Tables") {

View File

@@ -4,7 +4,6 @@
export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens",
GetAccessToken = "GetAccessToken",
Ready = "Ready",
}

View File

@@ -1,9 +1,47 @@
import { AuthorizationToken } from "./FabricMessageTypes";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
// This is the version of these messages
export const FABRIC_RPC_VERSION = "FabricMessageV3";
export const FABRIC_RPC_VERSION = "2";
// 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";
@@ -31,7 +69,7 @@ export type FabricMessageV2 =
message: {
id: string;
error: string | undefined;
data: ResourceTokenInfo | undefined;
data: FabricDatabaseConnectionInfo | undefined;
};
}
| {
@@ -41,81 +79,17 @@ export type FabricMessageV2 =
};
};
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 CosmosDBTokenResponse = {
token: string;
date: string;
};
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 {
export type CosmosDBConnectionInfoResponse = {
endpoint: string;
databaseId: string;
resourceTokens: Record<string, string> | undefined;
accessToken: string | undefined;
isReadOnly: boolean;
credentialType: "Key" | "OAuth2" | undefined;
}
resourceTokens: { [resourceId: string]: string };
};
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
resourceTokensTimestamp: number;
}

View File

@@ -1,7 +1,5 @@
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";
@@ -21,6 +19,7 @@ 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";
@@ -42,7 +41,7 @@ export interface DatabaseContextMenuButtonParams {
* New resource tree (in ReactJS)
*/
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
if (isFabric() && userContext.fabricContext?.isReadOnly) {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
return undefined;
}
@@ -54,7 +53,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
},
];
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
@@ -146,7 +145,7 @@ export const createCollectionContextMenuButton = (
});
}
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
if (configContext.platform !== Platform.Fabric) {
items.push({
iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {

View File

@@ -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 { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
import { scheduleRefreshDatabaseResourceToken } 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 { ReactTabKind, useTabs } from "../hooks/useTabs";
import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
@@ -187,10 +187,6 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
}
if (isFabricMirrored()) {
useTabs.getState().closeReactTab(ReactTabKind.Home);
}
this.refreshExplorer();
}
@@ -351,8 +347,8 @@ export default class Explorer {
};
public onRefreshResourcesClick = async (): Promise<void> => {
if (isFabricMirroredKey()) {
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
if (configContext.platform === Platform.Fabric) {
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
return;
}

View File

@@ -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,18 +93,19 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
);
}
const rootStyle = isFabric()
? {
root: {
backgroundColor: "transparent",
padding: "2px 8px 0px 8px",
},
}
: {
root: {
backgroundColor: backgroundColor,
},
};
const rootStyle =
configContext.platform === Platform.Fabric
? {
root: {
backgroundColor: "transparent",
padding: "2px 8px 0px 8px",
},
}
: {
root: {
backgroundColor: backgroundColor,
},
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);

View File

@@ -36,10 +36,6 @@
&:active {
background-color:@NotificationHigh;
}
&:focus {
.focusedBorder();
}
.statusBar {
.dataTypeIcons {

View File

@@ -81,6 +81,10 @@ 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,
@@ -97,9 +101,7 @@ export class NotificationConsoleComponent extends React.Component<
<div
className="notificationConsoleHeader"
id="notificationConsoleHeader"
role="button"
aria-label="Console"
aria-expanded={this.props.isConsoleExpanded}
ref={this.setElememntRef}
onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0}
@@ -127,7 +129,14 @@ export class NotificationConsoleComponent extends React.Component<
</span>
</span>
</div>
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
<div
className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
aria-label="Console"
aria-expanded={this.props.isConsoleExpanded}
>
<img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
@@ -250,6 +259,9 @@ export class NotificationConsoleComponent extends React.Component<
}
private onConsoleWasExpanded = (): void => {
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
this.consoleHeaderElement.focus();
}
useNotificationConsole.getState().setConsoleAnimationFinished(true);
};

View File

@@ -5,13 +5,10 @@ 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
@@ -74,8 +71,12 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</span>
</div>
<div
aria-expanded={false}
aria-label="Console"
className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
>
<img
alt="Expand icon"
@@ -175,13 +176,10 @@ 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
@@ -246,8 +244,12 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</span>
</div>
<div
aria-expanded={false}
aria-label="Console"
className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
>
<img
alt="Expand icon"

View File

@@ -2,7 +2,7 @@
* Notebook container related stuff
*/
import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError, Options } from "p-retry";
import promiseRetry, { AbortError } 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: Options;
private retryOptions: promiseRetry.Options;
private scheduleTimerId: NodeJS.Timeout;
constructor(private onConnectionLost: () => void) {

View File

@@ -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 (
isFabricMirrored() &&
configContext.platform === Platform.Fabric &&
!(
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored
// whitelist the tab kinds that are allowed to be opened in Fabric
(
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind.SQLQuery

View File

@@ -28,7 +28,6 @@ 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";
@@ -42,7 +41,6 @@ import {
isVectorSearchEnabled,
} from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import "../Controls/ThroughputInput/ThroughputInput.less";
@@ -286,152 +284,150 @@ 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">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Database {userContext.apiType === "Mongo" ? "name" : "id"}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
<Stack hidden={userContext.apiType === "Tables"}>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Database {userContext.apiType === "Mongo" ? "name" : "id"}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
/>
</TooltipHost>
</Stack>
{configContext.platform !== Platform.Fabric && (
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={this.state.createNewDatabase}
aria-label="Create new database"
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
checked={!this.state.createNewDatabase}
aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
)}
{this.state.createNewDatabase && (
<Stack className="panelGroupSpacing">
{configContext.platform !== Platform.Fabric && (
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
name="newDatabaseId"
id="newDatabaseId"
aria-required
required
type="text"
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder="Type a new database id"
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
autoFocus
className="panelRadioBtn"
checked={this.state.createNewDatabase}
aria-label="Create new database"
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Create new</span>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ isSharedThroughputChecked: isChecked })
}
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
<input
className="panelRadioBtn"
checked={!this.state.createNewDatabase}
aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
)}
{this.state.createNewDatabase && (
<Stack className="panelGroupSpacing">
<input
name="newDatabaseId"
id="newDatabaseId"
aria-required
required
type="text"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new database id"
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
autoFocus
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
}
/>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ isSharedThroughputChecked: isChecked })
}
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
)}
/>
</TooltipHost>
</Stack>
)}
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/>
)}
</Stack>
)}
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database"
options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string })
}
defaultSelectedKey={this.props.databaseId}
responsiveMode={999}
/>
)}
<Separator className="panelSeparator" />
</Stack>
)}
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/>
)}
</Stack>
)}
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database"
options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string })
}
defaultSelectedKey={this.props.databaseId}
responsiveMode={999}
/>
)}
<Separator className="panelSeparator" />
</Stack>
<Stack>
<Stack horizontal>
@@ -460,8 +456,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-required
required
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
@@ -670,7 +666,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
);
})}
{!isFabricNative() && userContext.apiType === "SQL" && (
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
@@ -751,7 +747,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
)}
{!isFabricNative() && userContext.apiType === "SQL" && (
{userContext.apiType === "SQL" && (
<Stack>
<Stack horizontal>
<Text className="panelTextBold" variant="small">
@@ -941,7 +937,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</CollapsibleSectionComponent>
</Stack>
)}
{!isFabricNative() && userContext.apiType !== "Tables" && (
{userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
@@ -1264,7 +1260,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// }
private shouldShowCollectionThroughputInput(): boolean {
if (isFabricNative() || isServerlessAccount()) {
if (isServerlessAccount()) {
return false;
}
@@ -1290,7 +1286,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowAnalyticalStoreOptions(): boolean {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
if (configContext.platform === Platform.Emulator) {
return false;
}

View File

@@ -1,6 +1,5 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
@@ -205,8 +204,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
type="text"
aria-required="true"
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size={40}
aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder}

View File

@@ -39,7 +39,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
data-lpignore={true}
id="database-id"
onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
pattern="[^/?#\\\\]*[^/?# \\\\]"
placeholder="Type a new database id"
size={40}
styles={

View File

@@ -7,7 +7,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
@@ -203,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true}
autoComplete="off"
styles={getTextFieldStyles()}
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Type a new keyspace id"
size={40}
value={newKeyspaceId}
@@ -293,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true}
ariaLabel="addCollection-table Id Create table"
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\-]*[^/?#- \\]"
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Enter table Id"
size={20}
value={tableId}

View File

@@ -28,7 +28,6 @@ import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
import { useDatabases } from "Explorer/useDatabases";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel";
import * as React from "react";
@@ -236,8 +235,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
aria-required
required
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"

View File

@@ -93,7 +93,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
id="newDatabaseId"
name="newDatabaseId"
onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
pattern="[^/?#\\\\]*[^/?# \\\\]"
placeholder="Type a new database id"
required={true}
size={40}
@@ -178,7 +178,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
id="collectionId"
name="collectionId"
onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
pattern="[^/?#\\\\]*[^/?# \\\\]"
placeholder="e.g., Container1"
required={true}
size={40}

View File

@@ -18,7 +18,7 @@ import {
Text,
TextField,
} from "@fluentui/react";
import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import QueryError, { QueryErrorSeverity } from "Common/QueryError";
import { createUri } from "Common/UrlUtility";
@@ -579,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 }}>{FeedbackLabels.provideFeedback}</Text>
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
role="status"
@@ -629,9 +629,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<IconButton
id="likeBtn"
style={{ marginLeft: 10 }}
aria-label={FeedbackLabels.provideFeedback}
role="button"
title="Like"
aria-label="Like"
role="toggle"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setShowCallout(!likeQuery);
@@ -649,9 +648,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
/>
<IconButton
style={{ margin: "0 4px" }}
role="button"
aria-label={FeedbackLabels.provideFeedback}
title="Dislike"
role="toggle"
aria-label="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => {
let toggleStatusValue = "Unpressed";

View File

@@ -1,6 +1,5 @@
import {
Button,
makeStyles,
Menu,
MenuButton,
MenuButtonProps,
@@ -8,12 +7,13 @@ import {
MenuList,
MenuPopover,
MenuTrigger,
SplitButton,
makeStyles,
mergeClasses,
shorthands,
SplitButton,
} from "@fluentui/react-components";
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
import { configContext, Platform } from "ConfigContext";
import { Platform, configContext } from "ConfigContext";
import Explorer from "Explorer/Explorer";
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
import { Tabs } from "Explorer/Tabs/Tabs";
@@ -21,7 +21,6 @@ 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";
@@ -124,7 +123,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
const actions = useMemo<GlobalCommand[]>(() => {
if (
(isFabric() && userContext.fabricContext?.isReadOnly) ||
configContext.platform === Platform.Fabric ||
userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo"
) {
@@ -138,15 +137,12 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
id: "new_collection",
label: `New ${getCollectionName()}`,
icon: <Add16Regular />,
onClick: () => {
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
explorer.onNewCollectionClicked({ databaseId });
},
onClick: () => explorer.onNewCollectionClicked(),
keyboardAction: KeyboardAction.NEW_COLLECTION,
},
];
if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") {
if (userContext.apiType !== "Tables") {
actions.push({
id: "new_database",
label: `New ${getDatabaseName()}`,
@@ -292,7 +288,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
}, [setLoading]);
const hasGlobalCommands = !(
isFabricMirrored() ||
configContext.platform === Platform.Fabric ||
userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo"
);

View File

@@ -1,173 +0,0 @@
/**
* 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>
);
};

View File

@@ -2,7 +2,6 @@ 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";
@@ -342,15 +341,10 @@ describe("Documents tab (noSql API)", () => {
updateConfigContext({ platform: Platform.Fabric });
updateUserContext({
fabricContext: {
databaseName: "database",
artifactInfo: {
connectionId: "test",
resourceTokenInfo: undefined,
},
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
connectionId: "test",
databaseConnectionInfo: undefined,
isReadOnly: true,
isVisible: true,
fabricClientRpcVersion: "rpcVersion",
},
});

View File

@@ -20,6 +20,7 @@ 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";
@@ -42,7 +43,6 @@ 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";
@@ -55,7 +55,6 @@ import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
import NewDocumentIcon from "../../../../images/NewDocument.svg";
import UploadIcon from "../../../../images/Upload_16x16.svg";
import DiscardIcon from "../../../../images/discard.svg";
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import * as Constants from "../../../Common/Constants";
import * as HeadersUtility from "../../../Common/HeadersUtility";
@@ -132,14 +131,6 @@ export const useDocumentsTabStyles = makeStyles({
backgroundColor: "white",
zIndex: 1,
},
refreshBtn: {
position: "absolute",
top: "3px",
right: "4px",
float: "right",
zIndex: 1,
backgroundColor: "transparent",
},
deleteProgressContent: {
paddingTop: tokens.spacingVerticalL,
},
@@ -353,7 +344,7 @@ export const getTabsButtons = ({
onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick,
}: ButtonsDependencies): CommandButtonComponentProps[] => {
if (isFabric() && userContext.fabricContext?.isReadOnly) {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
// All the following buttons require write access
return [];
}
@@ -2145,7 +2136,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedColumnIds={selectedColumnIds}
columnDefinitions={columnDefinitions}
isRowSelectionDisabled={
isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
isBulkDeleteDisabled ||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
}
onColumnSelectionChange={onColumnSelectionChange}
defaultColumnSelection={getInitialColumnSelection()}
@@ -2153,18 +2145,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
isColumnSelectionDisabled={isPreferredApiMongoDB}
/>
</div>
{tableContainerSizePx?.width >= calculateOffset(selectedColumnIds.length) + 200 && (
<div
title="Refresh"
className={styles.refreshBtn}
role="button"
onClick={() => refreshDocumentsGrid(false)}
aria-label="Refresh"
tabIndex={0}
>
<img src={RefreshIcon} alt="Refresh" />
</div>
)}
</div>
{tableItems.length > 0 && (
<a

View File

@@ -233,7 +233,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
aria-label="Select column"
size="small"
icon={<MoreHorizontalRegular />}
style={{ position: "absolute", right: 10, backgroundColor: tokens.colorNeutralBackground1 }}
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
/>
</MenuTrigger>
<MenuPopover>

View File

@@ -1,6 +1,5 @@
import * as ko from "knockout";
import Q from "q";
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
@@ -58,7 +57,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
}
this.id = editable.observable<string>();
this.id.validations([IsValidCosmosDbResourceId]);
this.id.validations([ScriptTabBase._isValidId]);
this.editorContent = editable.observable<string>();
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
@@ -263,6 +262,29 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
this.updateNavbarWithTabsButtons();
}
private static _isValidId(id: string): boolean {
if (!id) {
return false;
}
const invalidStartCharacters = /^[/?#\\]/;
if (invalidStartCharacters.test(id)) {
return false;
}
const invalidMiddleCharacters = /^.+[/?#\\]/;
if (invalidMiddleCharacters.test(id)) {
return false;
}
const invalidEndCharacters = /.*[/?#\\ ]$/;
if (invalidEndCharacters.test(id)) {
return false;
}
return true;
}
private static _isNotEmpty(value: string): boolean {
return !!value;
}

View File

@@ -1,7 +1,6 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { Pivot, PivotItem } from "@fluentui/react";
import { KeyboardAction } from "KeyboardShortcuts";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React from "react";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import DiscardIcon from "../../../../images/discard.svg";
@@ -456,12 +455,11 @@ export default class StoredProcedureTabComponent extends React.Component<
}
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
const isValidId: boolean = event.currentTarget.reportValidity();
if (this.state.saveButton.visible) {
this.setState({
id: event.target.value,
saveButton: {
enabled: isValidId,
enabled: true,
visible: this.props.scriptTabBaseInstance.isNew(),
},
discardButton: {
@@ -530,8 +528,8 @@ export default class StoredProcedureTabComponent extends React.Component<
className="formTree"
type="text"
required
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
aria-label="Stored procedure id"
placeholder="Enter the new stored procedure id"
size={40}

View File

@@ -2,7 +2,6 @@ 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";
@@ -10,7 +9,6 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout";
@@ -273,11 +271,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
<ConnectTab />
);
case ReactTabKind.Home:
if (isFabricNative()) {
return <FabricHomeScreen explorer={explorer} />;
} else {
return <SplashScreen explorer={explorer} />;
}
return <SplashScreen explorer={explorer} />;
case ReactTabKind.Quickstart:
return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoQuickstartTab explorer={explorer} />

View File

@@ -5,7 +5,6 @@ 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";
@@ -43,11 +42,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
return (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenPostgresNetworkingBlade}
screenshot={
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
? VcoreFirewallRuleScreenshot
: FirewallRuleScreenshot
}
screenshot={FirewallRuleScreenshot}
shellName={this.getShellNameForDisplay(this.kind)}
/>
);

View File

@@ -1,7 +1,6 @@
import { TriggerDefinition } from "@azure/cosmos";
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
import { KeyboardAction } from "KeyboardShortcuts";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
@@ -193,6 +192,29 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
});
}
private isValidId(id: string): boolean {
if (!id) {
return false;
}
const invalidStartCharacters = /^[/?#\\]/;
if (invalidStartCharacters.test(id)) {
return false;
}
const invalidMiddleCharacters = /^.+[/?#\\]/;
if (invalidMiddleCharacters.test(id)) {
return false;
}
const invalidEndCharacters = /.*[/?#\\ ]$/;
if (invalidEndCharacters.test(id)) {
return false;
}
return true;
}
private isNotEmpty(value: string): boolean {
return !!value;
}
@@ -264,13 +286,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string,
): void => {
const inputElement = _event.currentTarget as HTMLInputElement;
let isValidId: boolean = true;
if (inputElement) {
isValidId = inputElement.reportValidity();
}
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
this.setState({ triggerId: newValue });
};
@@ -297,8 +313,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
autoFocus
required
type="text"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\]*[^/?# \\]"
placeholder="Enter the new trigger id"
size={40}
value={triggerId}

View File

@@ -1,7 +1,6 @@
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
import { Label, TextField } from "@fluentui/react";
import { KeyboardAction } from "KeyboardShortcuts";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
@@ -65,13 +64,7 @@ export default class UserDefinedFunctionTabContent extends Component<
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string,
): void => {
const inputElement = _event.currentTarget as HTMLInputElement;
let isValidId: boolean = true;
if (inputElement) {
isValidId = inputElement.reportValidity();
}
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
this.setState({ udfId: newValue });
};
@@ -245,6 +238,29 @@ export default class UserDefinedFunctionTabContent extends Component<
});
}
private isValidId(id: string): boolean {
if (!id) {
return false;
}
const invalidStartCharacters = /^[/?#\\]/;
if (invalidStartCharacters.test(id)) {
return false;
}
const invalidMiddleCharacters = /^.+[/?#\\]/;
if (invalidMiddleCharacters.test(id)) {
return false;
}
const invalidEndCharacters = /.*[/?#\\ ]$/;
if (invalidEndCharacters.test(id)) {
return false;
}
return true;
}
private isNotEmpty(value: string): boolean {
return !!value;
}
@@ -268,8 +284,7 @@ export default class UserDefinedFunctionTabContent extends Component<
required
readOnly={!isUdfIdEditable}
type="text"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
pattern="[^/?#\\]*[^/?# \\]"
placeholder="Enter the new user defined function id"
size={40}
value={udfId}

View File

@@ -1,7 +1,6 @@
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";
@@ -35,6 +34,7 @@ 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 =
!isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);

View File

@@ -1,6 +1,7 @@
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 {
@@ -10,7 +11,6 @@ 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,22 +76,23 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
: [];
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
const headerNodes: TreeNode[] = isFabricMirrored()
? []
: [
{
id: "home",
iconSrc: <Home16Regular />,
label: "Home",
isSelected: () =>
useSelectedNode.getState().selectedNode === undefined &&
useTabs.getState().activeReactTab === ReactTabKind.Home,
onClick: () => {
useSelectedNode.getState().setSelectedNode(undefined);
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
const headerNodes: TreeNode[] =
configContext.platform === Platform.Fabric
? []
: [
{
id: "home",
iconSrc: <Home16Regular />,
label: "Home",
isSelected: () =>
useSelectedNode.getState().selectedNode === undefined &&
useTabs.getState().activeReactTab === ReactTabKind.Home,
onClick: () => {
useSelectedNode.getState().setSelectedNode(undefined);
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
},
},
},
];
];
const rootNodes: TreeNode[] = useMemo(() => {
if (sampleDataNodes.length > 0) {

View File

@@ -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 non read-only 1`] = `
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = `
[
{
"children": [
@@ -753,12 +753,6 @@ 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}
@@ -780,12 +774,6 @@ 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}
@@ -834,12 +822,6 @@ 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}
@@ -888,12 +870,6 @@ 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}
@@ -939,145 +915,6 @@ 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`] = `
[
{
@@ -1135,7 +972,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
},
],
"isSelected": [Function],
"label": "mockSproc4",
"label": "mockSproc3",
"onClick": [Function],
},
],
@@ -1153,7 +990,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
},
],
"isSelected": [Function],
"label": "mockUdf4",
"label": "mockUdf3",
"onClick": [Function],
},
],
@@ -1171,7 +1008,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
},
],
"isSelected": [Function],
"label": "mockTrigger4",
"label": "mockTrigger3",
"onClick": [Function],
},
],

View File

@@ -1,6 +1,5 @@
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";
@@ -17,7 +16,7 @@ import {
} from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { FabricContext, updateUserContext, UserContext } from "UserContext";
import { updateUserContext } from "UserContext";
import PromiseSource from "Utils/PromiseSource";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
@@ -361,30 +360,9 @@ describe("createDatabaseTreeNodes", () => {
});
});
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,
},
],
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 }],
[
"the Cassandra API, serverless, on Hosted",
Platform.Hosted,
@@ -395,7 +373,6 @@ describe("createDatabaseTreeNodes", () => {
{ name: CapabilityNames.EnableServerless, description: "" },
],
},
{ fabricContext: undefined },
],
[
"the Mongo API, with Notebooks and Phoenix features, on Emulator",
@@ -404,31 +381,26 @@ describe("createDatabaseTreeNodes", () => {
{
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
},
{ fabricContext: undefined },
],
])(
"generates the correct tree structure for %s",
(_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
updateConfigContext({ platform });
updateUserContext({
...userContext,
databaseAccount: {
properties: {
enableMultipleWriteLocations: true,
...dbAccountProperties,
},
} as unknown as DataModels.DatabaseAccount,
});
const nodes = createDatabaseTreeNodes(
explorer,
isNotebookEnabled,
useDatabases.getState().databases,
refreshActiveTab,
);
expect(nodes).toMatchSnapshot();
},
);
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => {
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
updateConfigContext({ platform });
updateUserContext({
databaseAccount: {
properties: {
enableMultipleWriteLocations: true,
...dbAccountProperties,
},
} as unknown as DataModels.DatabaseAccount,
});
const nodes = createDatabaseTreeNodes(
explorer,
isNotebookEnabled,
useDatabases.getState().databases,
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.
@@ -579,18 +551,7 @@ describe("createDatabaseTreeNodes", () => {
});
it.each([
[
"in Fabric",
() => {
updateConfigContext({ platform: Platform.Fabric });
updateUserContext({
fabricContext: {
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
isReadOnly: true,
} as FabricContext<CosmosDbArtifactType>,
});
},
],
["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })],
[
"for Cassandra API",
() =>

View File

@@ -6,7 +6,6 @@ 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";
@@ -23,7 +22,9 @@ import { useNotebook } from "../Notebook/useNotebook";
import { useSelectedNode } from "../useSelectedNode";
export const shouldShowScriptNodes = (): boolean => {
return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
return (
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
};
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;

View File

@@ -1,71 +1,48 @@
{
"MaterializedViewsBuilderDescription": "Provision a materialized views builder cluster for your Azure Cosmos DB account. Materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
"MaterializedViewsBuilder": "Materialized views Builder",
"Provisioned": "Provisioned",
"Deprovisioned": "Deprovisioned",
"LearnAboutMaterializedViews": "Learn more about materialized views.",
"DeprovisioningDetailsText": "Learn more about materialized views.",
"MaterializedviewsBuilderPricing": "Learn more about materialized views pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Materialized views builder resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Materialized views Builder resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Materialized views Builder resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Materialized views Builder resource provisioning failed.",
"UpdateMessage": "Materialized views builder resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Materialized views Builder resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Materialized views Builder resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Materialized views Builder resource updation failed.",
"DeleteMessage": "Materialized views builder resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Materialized views Builder resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Materialized views Builder resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Materialized views Builder resource deletion failed.",
"ApproximateCost": "Approximate Cost Per Hour",
"CostText": "Hourly cost of the materialized views Builder resource depends on the SKU selection and number of instances per region.",
"MetricsString": "Metrics",
"MetricsText": "Monitor the CPU and memory usage for the materialized views Builder instances in ",
"MetricsBlade": "the metrics blade.",
"MonitorUsage": "Monitor Usage",
"ResizingDecisionText": "To understand if the materialized views Builder is the right size, ",
"ResizingDecisionLink": "learn more about materialized views Builder sizing.",
"WarningBannerOnUpdate": "Adding or modifying materialized views Builder instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the materialized views Builder, your materialized views will not be updated with new source changes anymore. materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
"GlobalsecondaryindexesBuilderDescription": "Provision a global secondary indexes builder for your Azure Cosmos DB account. The global secondary indexes builder is compute in your account that performs read operations on source collections for any updates and populates the global secondary indexes as per their definition.",
"GlobalsecondaryindexesBuilder": "Global secondary indexes builder",
"LearnAboutGlobalSecondaryIndexes": "Learn more about global secondary indexes.",
"GlobalsecondaryindexesDeprovisioningDetailsText": "Learn more about global secondary indexes.",
"GlobalsecondaryindexesBuilderPricing": "Learn more about global secondary indexes pricing.",
"GlobalsecondaryindexesCreateMessage": "Global secondary indexes builder resource is being created.",
"GlobalsecondaryindexesCreateInitializeMessage": "Global secondary indexes builder resource will be provisioned.",
"GlobalsecondaryindexesCreateSuccesseMessage": "Global secondary indexes builder resource provisioned.",
"GlobalsecondaryindexesCreateFailureMessage": "Global secondary indexes builder resource provisioning failed.",
"GlobalsecondaryindexesUpdateMessage": "Global secondary indexes builder resource is being updated.",
"GlobalsecondaryindexesUpdateInitializeMessage": "Global secondary indexes builder resource will be updated.",
"GlobalsecondaryindexesUpdateSuccesseMessage": "Global secondary indexes builder resource updated.",
"GlobalsecondaryindexesUpdateFailureMessage": "Global secondary indexes builder resource update failed.",
"GlobalsecondaryindexesDeleteMessage": "Global secondary indexes builder resource is being deleted.",
"GlobalsecondaryindexesDeleteInitializeMessage": "Global secondary indexes builder resource will be deleted.",
"GlobalsecondaryindexesDeleteSuccesseMessage": "Global secondary indexes builder resource deleted.",
"GlobalsecondaryindexesDeleteFailureMessage": "Global secondary indexes builder resource deletion failed.",
"GlobalsecondaryindexesCostText": "Hourly cost of the global secondary indexes builder resource depends on the SKU selection and number of instances per region.",
"GlobalsecondaryindexesMetricsText": "Monitor the CPU and memory usage for the global secondary indexes builder instances in ",
"GlobalsecondaryindexesResizingDecisionText": "To understand if the global secondary indexes builder is the right size, ",
"GlobalsecondaryindexesesizingDecisionLink": "learn more about global secondary indexes builder sizing.",
"GlobalsecondaryindexesWarningBannerOnUpdate": "Adding or modifying global secondary indexes builder instances may affect your bill.",
"GlobalsecondaryindexesWarningBannerOnDelete": "After deprovisioning the global secondary indexes builder, your global secondary indexes will no longer be updated with new source changes. Global secondary indexes builder is compute in your account that performs read operations on source collection for any updates and applies them on global secondary indexes as per their definition."
"MaterializedViewsBuilderDescription": "Provision a Materializedviews builder cluster for your Azure Cosmos DB account. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
"MaterializedViewsBuilder": "Materializedviews Builder",
"Provisioned": "Provisioned",
"Deprovisioned": "Deprovisioned",
"LearnAboutMaterializedViews": "Learn more about materializedviews.",
"DeprovisioningDetailsText": "Learn more about materializedviews.",
"MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "MaterializedViewsBuilder resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Materializedviews Builder resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Materializedviews Builder resource provisioning failed.",
"UpdateMessage": "MaterializedViewsBuilder resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Materializedviews Builder resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Materializedviews Builder resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Materializedviews Builder resource updation failed.",
"DeleteMessage": "MaterializedViewsBuilder resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Materializedviews Builder resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Materializedviews Builder resource deletion failed.",
"ApproximateCost": "Approximate Cost Per Hour",
"CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.",
"MetricsString": "Metrics",
"MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ",
"MetricsBlade": "the metrics blade.",
"MonitorUsage": "Monitor Usage",
"ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ",
"ResizingDecisionLink": "learn more about Materializedviews Builder sizing.",
"WarningBannerOnUpdate": "Adding or modifying Materializedviews Builder instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the Materializedviews Builder, your materializedviews will not be updated with new source changes anymore. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition."
}

View File

@@ -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, Options } from "p-retry";
import promiseRetry, { AbortError } from "p-retry";
import {
Areas,
ConnectionStatusType,
@@ -35,26 +35,21 @@ import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
export class PhoenixClient {
private armResourceId: string;
private containerHealthHandler: NodeJS.Timeout;
private retryOptions: Options = {
private retryOptions: promiseRetry.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,
});
}
@@ -275,17 +270,6 @@ 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) {

View File

@@ -1,112 +1,56 @@
import { sendCachedDataMessage } from "Common/MessageHandler";
import { configContext, Platform } from "ConfigContext";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
import { 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 | undefined;
let timeoutId: NodeJS.Timeout;
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
let lastRequestTimestamp: number | undefined = undefined;
let lastRequestTimestamp: number = undefined;
/**
* Request fabric token:
* - Mirrored key and AAD: Database Resource Tokens
* - Native: AAD token
* @returns
*/
const requestFabricToken = async (): Promise<void> => {
const requestDatabaseResourceTokens = async (): Promise<void> => {
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
return;
}
lastRequestTimestamp = Date.now();
try {
if (isFabricMirrored()) {
await requestAndStoreDatabaseResourceTokens();
} else if (isFabricNative()) {
await requestAndStoreAccessToken();
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
FabricMessageTypes.GetAllResourceTokens,
[],
userContext.fabricContext.connectionId,
);
if (!userContext.databaseAccount.properties.documentEndpoint) {
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
}
scheduleRefreshFabricToken();
updateUserContext({
fabricContext: {
...userContext.fabricContext,
databaseConnectionInfo: fabricDatabaseConnectionInfo,
isReadOnly: true,
},
databaseAccount: { ...userContext.databaseAccount },
});
scheduleRefreshDatabaseResourceToken();
} catch (error) {
logConsoleError(error as string);
logConsoleError(error);
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 scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
return new Promise((resolve) => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
@@ -115,7 +59,7 @@ export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void>
timeoutId = setTimeout(
() => {
requestFabricToken().then(resolve);
requestDatabaseResourceTokens().then(resolve);
},
refreshNow ? 0 : TOKEN_VALIDITY_MS,
);
@@ -124,15 +68,6 @@ export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void>
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
scheduleRefreshFabricToken(true);
scheduleRefreshDatabaseResourceToken(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;

View File

@@ -6,9 +6,9 @@ import { RefreshResult } from "../SelfServeTypes";
import MaterializedViewsBuilder from "./MaterializedViewsBuilder";
import {
FetchPricesResponse,
MaterializedViewsBuilderServiceResource,
PriceMapAndCurrencyCode,
RegionsResponse,
MaterializedViewsBuilderServiceResource,
UpdateMaterializedViewsBuilderRequestParameters,
} from "./MaterializedViewsBuilderTypes";
@@ -123,23 +123,11 @@ export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<Ref
if (response.properties.status === ResourceStatus.Running.toString()) {
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
return {
isUpdateInProgress: true,
updateInProgressMessageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateMessage" : "CreateMessage",
};
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
return {
isUpdateInProgress: true,
updateInProgressMessageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteMessage" : "DeleteMessage",
};
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
} else {
return {
isUpdateInProgress: true,
updateInProgressMessageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateMessage" : "UpdateMessage",
};
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
}
} catch {
//TODO differentiate between different failures

View File

@@ -29,20 +29,17 @@ import {
updateMaterializedViewsBuilderResource,
} from "./MaterializedViewsBuilder.rp";
import { userContext } from "../../UserContext";
const costPerHourDefaultValue: Description = {
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
textTKey: "CostText",
type: DescriptionType.Text,
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
};
const metricsStringValue: Description = {
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesMetricsText" : "MetricsText",
textTKey: "MetricsText",
type: DescriptionType.Text,
link: {
href: generateBladeLink(BladeType.Metrics),
@@ -79,8 +76,7 @@ const onNumberOfInstancesChange = (
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
} as Description,
hidden: false,
@@ -120,8 +116,7 @@ const onEnableMaterializedViewsBuilderChange = (
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
} as Description,
hidden: false,
@@ -134,17 +129,10 @@ const onEnableMaterializedViewsBuilderChange = (
} else {
currentValues.set("warningBanner", {
value: {
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesWarningBannerOnDelete" : "WarningBannerOnDelete",
textTKey: "WarningBannerOnDelete",
link: {
href:
userContext.apiType === "SQL"
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
textTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesDeprovisioningDetailsText"
: "DeprovisioningDetailsText",
href: "https://aka.ms/cosmos-db-materializedviews",
textTKey: "DeprovisioningDetailsText",
},
} as Description,
hidden: false,
@@ -194,19 +182,18 @@ const getInstancesMax = async (): Promise<number> => {
};
const NumberOfInstancesDropdownInfo: Info = {
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesResizingDecisionText" : "ResizingDecisionText",
messageTKey: "ResizingDecisionText",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesesizingDecisionLink" : "ResizingDecisionLink",
textTKey: "ResizingDecisionLink",
},
};
const ApproximateCostDropDownInfo: Info = {
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
messageTKey: "CostText",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
textTKey: "MaterializedviewsBuilderPricing",
},
};
@@ -281,20 +268,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
portalNotification: {
initialize: {
titleTKey: "DeleteInitializeTitle",
messageTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesDeleteInitializeMessage"
: "DeleteInitializeMessage",
messageTKey: "DeleteInitializeMessage",
},
success: {
titleTKey: "DeleteSuccessTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteSuccesseMessage" : "DeleteSuccesseMessage",
messageTKey: "DeleteSuccesseMessage",
},
failure: {
titleTKey: "DeleteFailureTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteFailureMessage" : "DeleteFailureMessage",
messageTKey: "DeleteFailureMessage",
},
},
};
@@ -307,20 +289,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
portalNotification: {
initialize: {
titleTKey: "UpdateInitializeTitle",
messageTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesUpdateInitializeMessage"
: "UpdateInitializeMessage",
messageTKey: "UpdateInitializeMessage",
},
success: {
titleTKey: "UpdateSuccessTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateSuccesseMessage" : "UpdateSuccesseMessage",
messageTKey: "UpdateSuccesseMessage",
},
failure: {
titleTKey: "UpdateFailureTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateFailureMessage" : "UpdateFailureMessage",
messageTKey: "UpdateFailureMessage",
},
},
};
@@ -334,20 +311,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
portalNotification: {
initialize: {
titleTKey: "CreateInitializeTitle",
messageTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesCreateInitializeMessage"
: "CreateInitializeMessage",
messageTKey: "CreateInitializeMessage",
},
success: {
titleTKey: "CreateSuccessTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateSuccesseMessage" : "CreateSuccesseMessage",
messageTKey: "CreateSuccesseMessage",
},
failure: {
titleTKey: "CreateFailureTitle",
messageTKey:
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateFailureMessage" : "CreateFailureMessage",
messageTKey: "CreateFailureMessage",
},
},
};
@@ -394,17 +366,11 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
@Values({
description: {
textTKey:
userContext.apiType === "SQL"
? "GlobalsecondaryindexesBuilderDescription"
: "MaterializedViewsBuilderDescription",
textTKey: "MaterializedViewsBuilderDescription",
type: DescriptionType.Text,
link: {
href:
userContext.apiType === "SQL"
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
textTKey: userContext.apiType === "SQL" ? "LearnAboutGlobalSecondaryIndexes" : "LearnAboutMaterializedViews",
href: "https://aka.ms/cosmos-db-materializedviews",
textTKey: "LearnAboutMaterializedViews",
},
},
})
@@ -412,7 +378,7 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
@OnChange(onEnableMaterializedViewsBuilderChange)
@Values({
labelTKey: userContext.apiType === "SQL" ? "GlobalSecondaryIndexesBuilder" : "MaterializedViewsBuilder",
labelTKey: "MaterializedViewsBuilder",
trueLabelTKey: "Provisioned",
falseLabelTKey: "Deprovisioned",
})

View File

@@ -11,24 +11,13 @@ import { updateUserContext } from "../UserContext";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import "./SelfServe.less";
import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeBaseClass, SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
initializeIcons();
const loadTranslationFile = async (
className: string | SelfServeBaseClass,
selfServeType?: SelfServeType,
): Promise<void> => {
const loadTranslationFile = async (className: string): Promise<void> => {
const language = i18n.languages[0];
let namespace: string; // className is used as a key to retrieve the localized strings
let fileName: string;
if (className instanceof SelfServeBaseClass) {
fileName = `${selfServeType}.json`;
namespace = className.constructor.name;
} else {
fileName = `${className}.json`;
namespace = className;
}
const fileName = `${className}.json`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let translations: any;
@@ -39,16 +28,12 @@ const loadTranslationFile = async (
} catch (e) {
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`);
}
i18n.addResourceBundle(language, namespace, translations.default, true);
i18n.addResourceBundle(language, className, translations.default, true);
};
const loadTranslations = async (
className: string | SelfServeBaseClass,
selfServeType: SelfServeType,
): Promise<void> => {
const loadTranslations = async (className: string): Promise<void> => {
await loadTranslationFile("Common");
await loadTranslationFile(className, selfServeType);
await loadTranslationFile(className);
};
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
@@ -56,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, selfServeType);
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, selfServeType);
await loadTranslations(selfServeType);
return sqlX.toSelfServeDescriptor();
}
case SelfServeType.graphapicompute: {
@@ -70,7 +55,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
);
const graphAPICompute = new GraphAPICompute.default();
await loadTranslations(graphAPICompute, selfServeType);
await loadTranslations(selfServeType);
return graphAPICompute.toSelfServeDescriptor();
}
case SelfServeType.materializedviewsbuilder: {
@@ -78,7 +63,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
);
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
await loadTranslations(materializedViewsBuilder, selfServeType);
await loadTranslations(selfServeType);
return materializedViewsBuilder.toSelfServeDescriptor();
}
default:

View File

@@ -10,7 +10,7 @@ import {
Text,
} from "@fluentui/react";
import { TFunction } from "i18next";
import promiseRetry, { AbortError, Options } from "p-retry";
import promiseRetry, { AbortError } 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: Options;
private retryOptions: promiseRetry.Options;
private translationFunction: TFunction;
componentDidMount(): void {

View File

@@ -197,11 +197,6 @@ 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 = {
@@ -242,7 +237,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
return { priceMap: undefined, billingCurrency: undefined };
}
};
@@ -291,6 +286,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
return new Map<string, Map<string, string>>();
return undefined;
}
};

View File

@@ -227,13 +227,11 @@ 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;

View File

@@ -17,7 +17,7 @@ export class JupyterLabAppFactory {
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
this.restartShell = true;
}
return content?.includes("cosmosshelluser@");
return content?.includes("cosmosuser@");
}
private isMongoShellStarted(content: string | undefined) {
@@ -68,6 +68,7 @@ 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) {
@@ -113,13 +114,6 @@ 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;
}
}

View File

@@ -1,4 +1,4 @@
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -47,21 +47,11 @@ export interface VCoreMongoConnectionParams {
connectionString: string;
}
export interface FabricArtifactInfo {
[CosmosDbArtifactType.MIRRORED_KEY]: {
connectionId: string;
resourceTokenInfo: ResourceTokenInfo | undefined;
};
[CosmosDbArtifactType.MIRRORED_AAD]: undefined;
[CosmosDbArtifactType.NATIVE]: undefined;
}
export interface FabricContext<T extends CosmosDbArtifactType> {
fabricClientRpcVersion: string;
interface FabricContext {
connectionId: string;
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
isReadOnly: boolean;
isVisible: boolean;
databaseName: string;
artifactType: CosmosDbArtifactType;
artifactInfo: FabricArtifactInfo[T];
}
export type AdminFeedbackControlPolicy =
@@ -80,7 +70,7 @@ export type AdminFeedbackPolicySettings = {
};
export interface UserContext {
readonly fabricContext?: FabricContext<CosmosDbArtifactType>;
readonly fabricContext?: FabricContext;
readonly authType?: AuthType;
readonly masterKey?: string;
readonly subscriptionId?: string;

View File

@@ -39,7 +39,6 @@ describe("AuthorizationUtils", () => {
it("should throw an error if token is malformed", () => {
expect(() =>
AuthorizationUtils.decryptJWTToken(
// This is an invalid JWT token used for testing
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.",
),
).toThrow();
@@ -48,7 +47,6 @@ describe("AuthorizationUtils", () => {
it("should return decrypted token payload", () => {
expect(
AuthorizationUtils.decryptJWTToken(
// This is an expired JWT token used for testing
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ",
),
).toBeDefined();

View File

@@ -1,18 +0,0 @@
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
const testCases = [
["validId", true],
["forward/slash", false],
["back\\slash", false],
["question?mark", false],
["hash#mark", false],
["?invalidstart", false],
["invalidEnd/", false],
["space-at-end ", false],
];
describe("IsValidCosmosDbResourceId", () => {
test.each(testCases)("IsValidCosmosDbResourceId(%p). Expected: %p", (id: string, expected: boolean) => {
expect(IsValidCosmosDbResourceId(id)).toBe(expected);
});
});

View File

@@ -1,24 +0,0 @@
//
// Common methods and constants for validation
//
//
// Validation of id for Cosmos DB resources:
// - Database
// - Container
// - Stored Procedure
// - User Defined Function (UDF)
// - Trigger
//
// Use these with <input> elements
// eslint-disable-next-line no-useless-escape
export const ValidCosmosDbIdInputPattern: RegExp = /[^\/?#\\]*[^\/?# \\]/;
export const ValidCosmosDbIdDescription: string = "May not end with space nor contain characters '\\' '/' '#' '?'";
// For a standalone function regex, we need to wrap the previous reg expression,
// to test against the entire value. This is done implicitly by input elements.
const ValidCosmosDbIdRegex: RegExp = new RegExp(`^(?:${ValidCosmosDbIdInputPattern.source})$`);
export function IsValidCosmosDbResourceId(id: string): boolean {
return id && ValidCosmosDbIdRegex.test(id);
}

View File

@@ -1,4 +1,3 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { Platform, configContext } from "./../ConfigContext";
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
@@ -8,7 +7,7 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
if (currentWindow.parent === currentWindow) {
return undefined;
}
if (isFabric() && currentWindow.parent.parent === currentWindow.top) {
if (configContext.platform === Platform.Fabric && currentWindow.parent.parent === currentWindow.top) {
// in Fabric data explorer is inside an extension iframe, so we have two parent iframes
return currentWindow;
}

View File

@@ -2,19 +2,11 @@ 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 {
ArtifactConnectionInfo,
CosmosDbArtifactType,
FABRIC_RPC_VERSION,
FabricMessageV2,
FabricMessageV3,
InitializeMessageV3,
} from "Contracts/FabricMessagesContract";
import { useDialog } from "Explorer/Controls/Dialog";
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
import Explorer from "Explorer/Explorer";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import {
AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME,
@@ -31,7 +23,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 { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
@@ -52,7 +44,7 @@ import {
} from "../Platform/Hosted/HostedUtils";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { FabricArtifactInfo, Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import {
acquireMsalTokenForAccount,
acquireTokenWithMsal,
@@ -112,7 +104,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 = ["2", FABRIC_RPC_VERSION];
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
let firstContainerOpened = false;
let explorer: Explorer;
@@ -128,7 +120,7 @@ async function configureFabric(): Promise<Explorer> {
return;
}
const data: FabricMessageV2 | FabricMessageV3 = event.data?.data;
const data: FabricMessageV2 = event.data?.data;
if (!data) {
return;
}
@@ -137,77 +129,38 @@ async function configureFabric(): Promise<Explorer> {
case "initialize": {
const fabricVersion = data.version;
if (!SUPPORTED_FABRIC_VERSIONS.includes(fabricVersion)) {
// 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");
// TODO Surface error to user
console.error(`Unsupported Fabric version: ${fabricVersion}`);
return;
}
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.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);
}
explorer = createExplorerFabric(data.message);
await scheduleRefreshDatabaseResourceToken(true);
resolve(explorer);
await explorer.refreshAllDatabases();
if (userContext.fabricContext.isVisible) {
firstContainerOpened = true;
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
}
break;
}
case "newContainer":
explorer.onNewCollectionClicked();
break;
case "authorizationToken":
case "allResourceTokens_v2":
case "accessToken": {
case "allResourceTokens_v2": {
handleCachedDataMessage(data);
break;
}
case "explorerVisible": {
userContext.fabricContext.isVisible = data.message.visible;
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
const { databaseName } = userContext.fabricContext;
if (databaseName !== undefined) {
firstContainerOpened = true;
openFirstContainer(explorer, databaseName);
}
if (
userContext.fabricContext.isVisible &&
!firstContainerOpened &&
userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined
) {
firstContainerOpened = true;
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
}
break;
}
@@ -467,29 +420,13 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
return 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,
};
function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer {
updateUserContext({
fabricContext: {
fabricClientRpcVersion,
connectionId: params.connectionId,
databaseConnectionInfo: undefined,
isReadOnly: true,
isVisible: params.isVisible ?? true,
databaseName: undefined,
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
artifactInfo,
},
authType: AuthType.ConnectionString,
databaseAccount: {
@@ -503,102 +440,11 @@ function createExplorerFabricLegacy(
},
},
});
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({

View File

@@ -1,7 +1,6 @@
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,
@@ -12,6 +11,7 @@ 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,11 +51,22 @@ 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: [] as TabsBase[],
openedReactTabs: [ReactTabKind.Home],
activeTab: undefined as TabsBase,
activeReactTab: ReactTabKind.Home,
openedTabs: [],
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
activeTab: undefined,
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
queryCopilotTabInitialInput: "",
isTabExecuting: false,
isQueryErrorThrown: false,
@@ -111,7 +122,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
}
return true;
});
if (updatedTabs.length === 0 && !isFabricMirrored()) {
if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
set({ activeTab: undefined, activeReactTab: undefined });
}
@@ -151,7 +162,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
}
});
if (get().openedTabs.length === 0 && !isFabricMirrored()) {
if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
set({ activeTab: undefined, activeReactTab: undefined });
}
}