mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-05 18:47:41 +00:00
Compare commits
27 Commits
users/sour
...
release/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7224dd26c1 | ||
|
|
4c73a1cc47 | ||
|
|
610da6a9a5 | ||
|
|
7812ca4914 | ||
|
|
bc4f18ba79 | ||
|
|
fd2551423d | ||
|
|
508abcd21c | ||
|
|
7774589d60 | ||
|
|
e23ba5ec8c | ||
|
|
75719b3cf0 | ||
|
|
f3f8fd241a | ||
|
|
2dc2e59162 | ||
|
|
6b811b5e76 | ||
|
|
69cf523274 | ||
|
|
2e45d8a2a4 | ||
|
|
8624bf0423 | ||
|
|
db1600d81b | ||
|
|
176bb47cb5 | ||
|
|
b6d17284b5 | ||
|
|
7b7a2817b6 | ||
|
|
f0e32491d7 | ||
|
|
c33c497fd9 | ||
|
|
cec621443d | ||
|
|
b8017763b7 | ||
|
|
59619a856e | ||
|
|
b1f016a796 | ||
|
|
ae3912cbf2 |
@@ -11,9 +11,3 @@ pool:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: ComponentGovernanceComponentDetection@0
|
- task: ComponentGovernanceComponentDetection@0
|
||||||
inputs:
|
|
||||||
scanType: 'Register'
|
|
||||||
verbosity: 'Verbose'
|
|
||||||
sourceScanPath: 'manifest'
|
|
||||||
detectorsFilter: 'cgmanifest'
|
|
||||||
alertWarningLevel: 'Low'
|
|
||||||
|
|||||||
@@ -1914,20 +1914,13 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
|
height: 32px;
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: -0.5px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
// Override the bootstrap defaults here to align with our layout constants.
|
|
||||||
margin-bottom: 0px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/component-detection-manifest.json",
|
|
||||||
"Registrations": [
|
|
||||||
{
|
|
||||||
"Component": {
|
|
||||||
"Type": "git",
|
|
||||||
"Git": {
|
|
||||||
"RepositoryUrl": "https://github.com/mongodb-js/mongosh",
|
|
||||||
"CommitHash": "6718ae4e76be007542087b8a674d7a77861c7d08"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DevelopmentDependency": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Component": {
|
|
||||||
"Type": "git",
|
|
||||||
"Git": {
|
|
||||||
"RepositoryUrl": "https://github.com/jeffwidman/cqlsh",
|
|
||||||
"CommitHash": "dbefab4f3082bd3525e9e39d836734fd905fb8df"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DevelopmentDependency": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
51
package-lock.json
generated
51
package-lock.json
generated
@@ -86,7 +86,7 @@
|
|||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "6.2.1",
|
"p-retry": "4.6.2",
|
||||||
"patch-package": "8.0.0",
|
"patch-package": "8.0.0",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
@@ -12662,9 +12662,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/retry": {
|
"node_modules/@types/retry": {
|
||||||
"version": "0.12.2",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
|
||||||
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/sanitize-html": {
|
"node_modules/@types/sanitize-html": {
|
||||||
@@ -21801,18 +21799,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/is-number": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -30257,20 +30243,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-retry": {
|
"node_modules/p-retry": {
|
||||||
"version": "6.2.1",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/retry": "0.12.2",
|
"@types/retry": "0.12.0",
|
||||||
"is-network-error": "^1.0.0",
|
|
||||||
"retry": "^0.13.1"
|
"retry": "^0.13.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.17"
|
"node": ">=8"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-try": {
|
"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": {
|
"node_modules/webpack-dev-server/node_modules/ajv": {
|
||||||
"version": "8.12.0",
|
"version": "8.12.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -36071,20 +36044,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "6.2.1",
|
"p-retry": "4.6.2",
|
||||||
"patch-package": "8.0.0",
|
"patch-package": "8.0.0",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
*/
|
*/
|
||||||
@@ -28,12 +29,7 @@ export default defineConfig({
|
|||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "chromium",
|
||||||
use: {
|
use: { ...devices["Desktop Chrome"] },
|
||||||
...devices["Desktop Chrome"],
|
|
||||||
launchOptions: {
|
|
||||||
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "firefox",
|
name: "firefox",
|
||||||
|
|||||||
37051
preview/package-lock.json
generated
37051
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -530,8 +530,8 @@ export class ariaLabelForLearnMoreLink {
|
|||||||
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GlobalSecondaryIndexLabels {
|
export class MaterializedViewsLabels {
|
||||||
public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index";
|
public static readonly NewMaterializedView: string = "New Materialized View";
|
||||||
}
|
}
|
||||||
export class FeedbackLabels {
|
export class FeedbackLabels {
|
||||||
public static readonly provideFeedback: string = "Provide feedback";
|
public static readonly provideFeedback: string = "Provide feedback";
|
||||||
|
|||||||
@@ -125,11 +125,7 @@ export const endpoint = () => {
|
|||||||
const location = _global.parent ? _global.parent.location : _global.location;
|
const location = _global.parent ? _global.parent.location : _global.location;
|
||||||
return configContext.EMULATOR_ENDPOINT || location.origin;
|
return configContext.EMULATOR_ENDPOINT || location.origin;
|
||||||
}
|
}
|
||||||
return (
|
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
|
||||||
userContext.selectedRegionalEndpoint ||
|
|
||||||
userContext.endpoint ||
|
|
||||||
userContext?.databaseAccount?.properties?.documentEndpoint
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getTokenFromAuthService(
|
export async function getTokenFromAuthService(
|
||||||
@@ -207,7 +203,6 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
userAgentSuffix: "Azure Portal",
|
userAgentSuffix: "Azure Portal",
|
||||||
defaultHeaders: _defaultHeaders,
|
defaultHeaders: _defaultHeaders,
|
||||||
connectionPolicy: {
|
connectionPolicy: {
|
||||||
enableEndpointDiscovery: !userContext.selectedRegionalEndpoint,
|
|
||||||
retryOptions: {
|
retryOptions: {
|
||||||
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
|
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
|
||||||
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),
|
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { TagNames, WorkloadType } from "Common/Constants";
|
import { TagNames, WorkloadType } from "Common/Constants";
|
||||||
import { Tags } from "Contracts/DataModels";
|
import { Tags } from "Contracts/DataModels";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
function isVirtualNetworkFilterEnabled() {
|
function isVirtualNetworkFilterEnabled() {
|
||||||
@@ -28,8 +27,6 @@ export function getWorkloadType(): WorkloadType {
|
|||||||
return workloadType;
|
return workloadType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isGlobalSecondaryIndexEnabled(): boolean {
|
export function isMaterializedViewsEnabled(): boolean {
|
||||||
return (
|
return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews;
|
||||||
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { QueryOperationOptions } from "@azure/cosmos";
|
import { QueryOperationOptions } from "@azure/cosmos";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as Constants from "../Common/Constants";
|
|
||||||
import { QueryResults } from "../Contracts/ViewModels";
|
import { QueryResults } from "../Contracts/ViewModels";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
|
|
||||||
interface QueryResponse {
|
interface QueryResponse {
|
||||||
// [Todo] remove any
|
// [Todo] remove any
|
||||||
@@ -24,9 +21,7 @@ export function nextPage(
|
|||||||
firstItemIndex: number,
|
firstItemIndex: number,
|
||||||
queryOperationOptions?: QueryOperationOptions,
|
queryOperationOptions?: QueryOperationOptions,
|
||||||
): Promise<QueryResults> {
|
): Promise<QueryResults> {
|
||||||
TelemetryProcessor.traceStart(Action.ExecuteQuery);
|
|
||||||
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
||||||
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab });
|
|
||||||
const documents = response.resources;
|
const documents = response.resources;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { constructRpOptions } from "Common/dataAccess/createCollection";
|
import { constructRpOptions } from "Common/dataAccess/createCollection";
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } from "Contracts/DataModels";
|
import { Collection, CreateMaterializedViewsParams } from "Contracts/DataModels";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
|
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import {
|
import {
|
||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
} from "Utils/arm/generatedClients/cosmos/types";
|
} from "Utils/arm/generatedClients/cosmos/types";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise<Collection> => {
|
export const createMaterializedView = async (params: CreateMaterializedViewsParams): Promise<Collection> => {
|
||||||
const clearMessage = logConsoleProgress(
|
const clearMessage = logConsoleProgress(
|
||||||
`Creating a new global secondary index ${params.materializedViewId} for database ${params.databaseId}`,
|
`Creating a new materialized view ${params.materializedViewId} for database ${params.databaseId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
@@ -58,15 +58,11 @@ export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIn
|
|||||||
params.materializedViewId,
|
params.materializedViewId,
|
||||||
rpPayload,
|
rpPayload,
|
||||||
);
|
);
|
||||||
logConsoleInfo(`Successfully created global secondary index ${params.materializedViewId}`);
|
logConsoleInfo(`Successfully created materialized view ${params.materializedViewId}`);
|
||||||
|
|
||||||
return createResponse && (createResponse.properties.resource as Collection);
|
return createResponse && (createResponse.properties.resource as Collection);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(error, "CreateMaterializedView", `Error while creating materialized view ${params.materializedViewId}`);
|
||||||
error,
|
|
||||||
"CreateGlobalSecondaryIndex",
|
|
||||||
`Error while creating global secondary index ${params.materializedViewId}`,
|
|
||||||
);
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -14,11 +13,6 @@ import { readOfferWithSDK } from "./readOfferWithSDK";
|
|||||||
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||||
|
|
||||||
if (isFabric()) {
|
|
||||||
// Not exposing offers in Fabric
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
|
|||||||
@@ -81,13 +81,6 @@ export type FabricMessageV3 =
|
|||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
data: { accessToken: string };
|
data: { accessToken: string };
|
||||||
};
|
};
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "refreshResourceTree";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum CosmosDbArtifactType {
|
export enum CosmosDbArtifactType {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
JSONObject,
|
|
||||||
QueryMetrics,
|
QueryMetrics,
|
||||||
Resource,
|
Resource,
|
||||||
StoredProcedureDefinition,
|
StoredProcedureDefinition,
|
||||||
@@ -207,12 +206,6 @@ export interface Collection extends CollectionBase {
|
|||||||
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||||
bulkInsertDocuments(documents: JSONObject[]): Promise<{
|
|
||||||
numSucceeded: number;
|
|
||||||
numFailed: number;
|
|
||||||
numThrottled: number;
|
|
||||||
errors: string[];
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { GlobalSecondaryIndexLabels } from "Common/Constants";
|
import { MaterializedViewsLabels } from "Common/Constants";
|
||||||
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
|
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import {
|
import {
|
||||||
AddGlobalSecondaryIndexPanel,
|
AddMaterializedViewPanel,
|
||||||
AddGlobalSecondaryIndexPanelProps,
|
AddMaterializedViewPanelProps,
|
||||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -170,19 +170,19 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) {
|
if (isMaterializedViewsEnabled() && !selectedCollection.materializedViewDefinition()) {
|
||||||
items.push({
|
items.push({
|
||||||
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
label: MaterializedViewsLabels.NewMaterializedView,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
|
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
||||||
explorer: container,
|
explorer: container,
|
||||||
sourceContainer: selectedCollection,
|
sourceContainer: selectedCollection,
|
||||||
};
|
};
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
MaterializedViewsLabels.NewMaterializedView,
|
||||||
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
|
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -214,10 +214,8 @@ export const Dialog: FC = () => {
|
|||||||
{contentHtml}
|
{contentHtml}
|
||||||
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} />
|
<PrimaryButton {...primaryButtonProps} />
|
||||||
{secondaryButtonProps && (
|
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
|
||||||
<DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} />
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</FluentDialog>
|
</FluentDialog>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
ThroughputBucketsComponentProps,
|
ThroughputBucketsComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -45,11 +44,11 @@ import {
|
|||||||
ConflictResolutionComponent,
|
ConflictResolutionComponent,
|
||||||
ConflictResolutionComponentProps,
|
ConflictResolutionComponentProps,
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||||
import {
|
|
||||||
GlobalSecondaryIndexComponent,
|
|
||||||
GlobalSecondaryIndexComponentProps,
|
|
||||||
} from "./SettingsSubComponents/GlobalSecondaryIndexComponent";
|
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||||
|
import {
|
||||||
|
MaterializedViewComponent,
|
||||||
|
MaterializedViewComponentProps,
|
||||||
|
} from "./SettingsSubComponents/MaterializedViewComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps,
|
MongoIndexingPolicyComponentProps,
|
||||||
@@ -167,7 +166,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private shouldShowComputedPropertiesEditor: boolean;
|
private shouldShowComputedPropertiesEditor: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
private isGlobalSecondaryIndex: boolean;
|
private isMaterializedView: boolean;
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
private isFullTextSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
@@ -185,7 +184,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
this.isGlobalSecondaryIndex =
|
this.isMaterializedView =
|
||||||
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
@@ -1278,10 +1277,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
isReadOnly: isFabricNative(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const globalSecondaryIndexComponentProps: GlobalSecondaryIndexComponentProps = {
|
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
@@ -1349,10 +1347,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isGlobalSecondaryIndex) {
|
if (this.isMaterializedView) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
|
tab: SettingsV2TabTypes.MaterializedViewTab,
|
||||||
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
|
content: <MaterializedViewComponent {...materializedViewComponentProps} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { collection, container } from "../TestUtils";
|
|
||||||
import { GlobalSecondaryIndexComponent } from "./GlobalSecondaryIndexComponent";
|
|
||||||
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
|
||||||
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
|
||||||
|
|
||||||
describe("GlobalSecondaryIndexComponent", () => {
|
|
||||||
let testCollection: typeof collection;
|
|
||||||
let testExplorer: typeof container;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testCollection = { ...collection };
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders only the source component when materializedViewDefinition is missing", () => {
|
|
||||||
testCollection.materializedViews([
|
|
||||||
{ id: "view1", _rid: "rid1" },
|
|
||||||
{ id: "view2", _rid: "rid2" },
|
|
||||||
]);
|
|
||||||
testCollection.materializedViewDefinition(null);
|
|
||||||
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(true);
|
|
||||||
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders only the target component when materializedViews is missing", () => {
|
|
||||||
testCollection.materializedViews(null);
|
|
||||||
testCollection.materializedViewDefinition({
|
|
||||||
definition: "SELECT * FROM c WHERE c.id = 1",
|
|
||||||
sourceCollectionId: "source1",
|
|
||||||
sourceCollectionRid: "rid123",
|
|
||||||
});
|
|
||||||
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
|
|
||||||
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders neither component when both are missing", () => {
|
|
||||||
testCollection.materializedViews(null);
|
|
||||||
testCollection.materializedViewDefinition(null);
|
|
||||||
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
|
|
||||||
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { collection, container } from "../TestUtils";
|
||||||
|
import { MaterializedViewComponent } from "./MaterializedViewComponent";
|
||||||
|
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||||
|
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||||
|
|
||||||
|
describe("MaterializedViewComponent", () => {
|
||||||
|
let testCollection: typeof collection;
|
||||||
|
let testExplorer: typeof container;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testCollection = { ...collection };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders only the source component when materializedViewDefinition is missing", () => {
|
||||||
|
testCollection.materializedViews([
|
||||||
|
{ id: "view1", _rid: "rid1" },
|
||||||
|
{ id: "view2", _rid: "rid2" },
|
||||||
|
]);
|
||||||
|
testCollection.materializedViewDefinition(null);
|
||||||
|
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(true);
|
||||||
|
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders only the target component when materializedViews is missing", () => {
|
||||||
|
testCollection.materializedViews(null);
|
||||||
|
testCollection.materializedViewDefinition({
|
||||||
|
definition: "SELECT * FROM c WHERE c.id = 1",
|
||||||
|
sourceCollectionId: "source1",
|
||||||
|
sourceCollectionRid: "rid123",
|
||||||
|
});
|
||||||
|
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
||||||
|
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders neither component when both are missing", () => {
|
||||||
|
testCollection.materializedViews(null);
|
||||||
|
testCollection.materializedViewDefinition(null);
|
||||||
|
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
||||||
|
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,27 +2,22 @@ import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||||
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||||
|
|
||||||
export interface GlobalSecondaryIndexComponentProps {
|
export interface MaterializedViewComponentProps {
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexComponentProps> = ({
|
export const MaterializedViewComponent: React.FC<MaterializedViewComponentProps> = ({ collection, explorer }) => {
|
||||||
collection,
|
|
||||||
explorer,
|
|
||||||
}) => {
|
|
||||||
const isTargetContainer = !!collection?.materializedViewDefinition();
|
const isTargetContainer = !!collection?.materializedViewDefinition();
|
||||||
const isSourceContainer = !!collection?.materializedViews();
|
const isSourceContainer = !!collection?.materializedViews();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
||||||
{isSourceContainer && (
|
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following views defined for it.</Text>
|
||||||
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text>
|
|
||||||
)}
|
|
||||||
<Text>
|
<Text>
|
||||||
<Link
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -31,11 +26,11 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
|
|||||||
Learn more
|
Learn more
|
||||||
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
about how to define global secondary indexes and how to use them.
|
about how to define materialized views and how to use them.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}
|
{isSourceContainer && <MaterializedViewSourceComponent collection={collection} explorer={explorer} />}
|
||||||
{isTargetContainer && <GlobalSecondaryIndexTargetComponent collection={collection} />}
|
{isTargetContainer && <MaterializedViewTargetComponent collection={collection} />}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -2,9 +2,9 @@ import { PrimaryButton } from "@fluentui/react";
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { collection, container } from "../TestUtils";
|
import { collection, container } from "../TestUtils";
|
||||||
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||||
|
|
||||||
describe("GlobalSecondaryIndexSourceComponent", () => {
|
describe("MaterializedViewSourceComponent", () => {
|
||||||
let testCollection: typeof collection;
|
let testCollection: typeof collection;
|
||||||
let testExplorer: typeof container;
|
let testExplorer: typeof container;
|
||||||
|
|
||||||
@@ -13,23 +13,17 @@ describe("GlobalSecondaryIndexSourceComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
|
|
||||||
);
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
expect(wrapper.exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the PrimaryButton", () => {
|
it("renders the PrimaryButton", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
|
|
||||||
);
|
|
||||||
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates when new global secondary indexes are provided", () => {
|
it("updates when new materialized views are provided", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulating an update by modifying the observable directly
|
// Simulating an update by modifying the observable directly
|
||||||
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
|
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
|
||||||
@@ -1,30 +1,29 @@
|
|||||||
import { PrimaryButton } from "@fluentui/react";
|
import { PrimaryButton } from "@fluentui/react";
|
||||||
import { GlobalSecondaryIndexLabels } from "Common/Constants";
|
import { MaterializedViewsLabels } from "Common/Constants";
|
||||||
import { MaterializedView } from "Contracts/DataModels";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||||
import { AddGlobalSecondaryIndexPanel } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
import { AddMaterializedViewPanel } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
export interface GlobalSecondaryIndexSourceComponentProps {
|
export interface MaterializedViewSourceComponentProps {
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexSourceComponentProps> = ({
|
export const MaterializedViewSourceComponent: React.FC<MaterializedViewSourceComponentProps> = ({
|
||||||
collection,
|
collection,
|
||||||
explorer,
|
explorer,
|
||||||
}) => {
|
}) => {
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
||||||
|
|
||||||
const globalSecondaryIndexes: MaterializedView[] = collection?.materializedViews() ?? [];
|
const materializedViews = collection?.materializedViews() ?? [];
|
||||||
|
|
||||||
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedView[] with collection id.
|
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedViews[] with collection id.
|
||||||
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
|
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
|
||||||
let definition = "";
|
let definition = "";
|
||||||
let partitionKey: string[] = [];
|
let partitionKey: string[] = [];
|
||||||
@@ -32,8 +31,8 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
|||||||
useDatabases.getState().databases.find((database) => {
|
useDatabases.getState().databases.find((database) => {
|
||||||
const collection = database.collections().find((collection) => collection.id() === viewId);
|
const collection = database.collections().find((collection) => collection.id() === viewId);
|
||||||
if (collection) {
|
if (collection) {
|
||||||
const globalSecondaryIndexDefinition = collection.materializedViewDefinition();
|
const materializedViewDefinition = collection.materializedViewDefinition();
|
||||||
globalSecondaryIndexDefinition && (definition = globalSecondaryIndexDefinition.definition);
|
materializedViewDefinition && (definition = materializedViewDefinition.definition);
|
||||||
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
|
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -43,7 +42,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
|||||||
|
|
||||||
//JSON value for the editor using the fetched id and definitions.
|
//JSON value for the editor using the fetched id and definitions.
|
||||||
const jsonValue = JSON.stringify(
|
const jsonValue = JSON.stringify(
|
||||||
globalSecondaryIndexes.map((view) => {
|
materializedViews.map((view) => {
|
||||||
const { definition, partitionKey } = getViewDetails(view.id);
|
const { definition, partitionKey } = getViewDetails(view.id);
|
||||||
return {
|
return {
|
||||||
name: view.id,
|
name: view.id,
|
||||||
@@ -67,7 +66,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
|||||||
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
||||||
value: jsonValue,
|
value: jsonValue,
|
||||||
language: "json",
|
language: "json",
|
||||||
ariaLabel: "Global Secondary Index JSON",
|
ariaLabel: "Materialized Views JSON",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -98,14 +97,14 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
text="Add index"
|
text="Add view"
|
||||||
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
MaterializedViewsLabels.NewMaterializedView,
|
||||||
<AddGlobalSecondaryIndexPanel explorer={explorer} sourceContainer={collection} />,
|
<AddMaterializedViewPanel explorer={explorer} sourceContainer={collection} />,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -3,9 +3,9 @@ import { Collection } from "Contracts/ViewModels";
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { collection } from "../TestUtils";
|
import { collection } from "../TestUtils";
|
||||||
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||||
|
|
||||||
describe("GlobalSecondaryIndexTargetComponent", () => {
|
describe("MaterializedViewTargetComponent", () => {
|
||||||
let testCollection: Collection;
|
let testCollection: Collection;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -16,17 +16,17 @@ describe("GlobalSecondaryIndexTargetComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
|
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||||
expect(wrapper.exists()).toBe(true);
|
expect(wrapper.exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays the source container ID", () => {
|
it("displays the source container ID", () => {
|
||||||
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
|
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||||
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
|
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays the global secondary index definition", () => {
|
it("displays the materialized view definition", () => {
|
||||||
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
|
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||||
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
|
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2,14 +2,12 @@ import { Stack, Text } from "@fluentui/react";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
export interface GlobalSecondaryIndexTargetComponentProps {
|
export interface MaterializedViewTargetComponentProps {
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexTargetComponentProps> = ({
|
export const MaterializedViewTargetComponent: React.FC<MaterializedViewTargetComponentProps> = ({ collection }) => {
|
||||||
collection,
|
const materializedViewDefinition = collection?.materializedViewDefinition();
|
||||||
}) => {
|
|
||||||
const globalSecondaryIndexDefinition = collection?.materializedViewDefinition();
|
|
||||||
|
|
||||||
const textHeadingStyle = {
|
const textHeadingStyle = {
|
||||||
root: { fontWeight: "600", fontSize: 16 },
|
root: { fontWeight: "600", fontSize: 16 },
|
||||||
@@ -25,19 +23,19 @@ export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexT
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text>
|
<Text styles={textHeadingStyle}>Materialized View Settings</Text>
|
||||||
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
||||||
<Stack styles={valueBoxStyle}>
|
<Stack styles={valueBoxStyle}>
|
||||||
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
|
<Text>{materializedViewDefinition?.sourceCollectionId}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
|
<Text styles={{ root: { fontWeight: "600" } }}>Materialized view definition</Text>
|
||||||
<Stack styles={valueBoxStyle}>
|
<Stack styles={valueBoxStyle}>
|
||||||
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
|
<Text>{materializedViewDefinition?.definition}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -29,26 +29,16 @@ export interface PartitionKeyComponentProps {
|
|||||||
database: ViewModels.Database;
|
database: ViewModels.Database;
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
isReadOnly?: boolean; // true: cannot change partition key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
|
||||||
database,
|
|
||||||
collection,
|
|
||||||
explorer,
|
|
||||||
isReadOnly,
|
|
||||||
}) => {
|
|
||||||
const { dataTransferJobs } = useDataTransferJobs();
|
const { dataTransferJobs } = useDataTransferJobs();
|
||||||
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isReadOnly) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadDataTransferJobs = refreshDataTransferOperations;
|
const loadDataTransferJobs = refreshDataTransferOperations;
|
||||||
loadDataTransferJobs();
|
loadDataTransferJobs();
|
||||||
}, [isReadOnly]);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const currentJob = findPortalDataTransferJob();
|
const currentJob = findPortalDataTransferJob();
|
||||||
@@ -173,61 +163,56 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
{!isReadOnly && (
|
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
|
||||||
<>
|
source container for the entire duration of the partition key change process.
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>
|
<Link
|
||||||
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to
|
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
||||||
the source container for the entire duration of the partition key change process.
|
target="_blank"
|
||||||
<Link
|
underline
|
||||||
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
>
|
||||||
target="_blank"
|
Learn more
|
||||||
underline
|
</Link>
|
||||||
>
|
</MessageBar>
|
||||||
Learn more
|
<Text>
|
||||||
</Link>
|
To change the partition key, a new destination container must be created or an existing destination container
|
||||||
</MessageBar>
|
selected. Data will then be copied to the destination container.
|
||||||
<Text>
|
</Text>
|
||||||
To change the partition key, a new destination container must be created or an existing destination
|
{configContext.platform !== Platform.Emulator && (
|
||||||
container selected. Data will then be copied to the destination container.
|
<PrimaryButton
|
||||||
</Text>
|
styles={{ root: { width: "fit-content" } }}
|
||||||
{configContext.platform !== Platform.Emulator && (
|
text="Change"
|
||||||
<PrimaryButton
|
onClick={startPartitionkeyChangeWorkflow}
|
||||||
styles={{ root: { width: "fit-content" } }}
|
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||||
text="Change"
|
/>
|
||||||
onClick={startPartitionkeyChangeWorkflow}
|
)}
|
||||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
{portalDataTransferJob && (
|
||||||
/>
|
<Stack>
|
||||||
)}
|
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||||
{portalDataTransferJob && (
|
<Stack
|
||||||
<Stack>
|
horizontal
|
||||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
tokens={{ childrenGap: 20 }}
|
||||||
<Stack
|
styles={{
|
||||||
horizontal
|
root: {
|
||||||
tokens={{ childrenGap: 20 }}
|
alignItems: "center",
|
||||||
styles={{
|
},
|
||||||
root: {
|
}}
|
||||||
alignItems: "center",
|
>
|
||||||
},
|
<ProgressIndicator
|
||||||
}}
|
label={portalDataTransferJob?.properties?.jobName}
|
||||||
>
|
description={getProgressDescription()}
|
||||||
<ProgressIndicator
|
percentComplete={getPercentageComplete()}
|
||||||
label={portalDataTransferJob?.properties?.jobName}
|
styles={{
|
||||||
description={getProgressDescription()}
|
root: {
|
||||||
percentComplete={getPercentageComplete()}
|
width: "85%",
|
||||||
styles={{
|
},
|
||||||
root: {
|
}}
|
||||||
width: "85%",
|
></ProgressIndicator>
|
||||||
},
|
{isCurrentJobInProgress(portalDataTransferJob) && (
|
||||||
}}
|
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
||||||
></ProgressIndicator>
|
)}
|
||||||
{isCurrentJobInProgress(portalDataTransferJob) && (
|
</Stack>
|
||||||
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
</Stack>
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export enum SettingsV2TabTypes {
|
|||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
ThroughputBucketsTab,
|
||||||
GlobalSecondaryIndexTab,
|
MaterializedViewTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
export enum ContainerPolicyTabTypes {
|
||||||
@@ -172,8 +172,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
return "Container Policies";
|
return "Container Policies";
|
||||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||||
return "Throughput Buckets";
|
return "Throughput Buckets";
|
||||||
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
|
case SettingsV2TabTypes.MaterializedViewTab:
|
||||||
return "Global Secondary Index (Preview)";
|
return "Materialized Views (Preview)";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,7 +306,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isReadOnly={false}
|
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
@@ -344,16 +343,16 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Global Secondary Index (Preview)"
|
headerText="Materialized Views (Preview)"
|
||||||
itemKey="GlobalSecondaryIndexTab"
|
itemKey="MaterializedViewTab"
|
||||||
key="GlobalSecondaryIndexTab"
|
key="MaterializedViewTab"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"marginTop": 20,
|
"marginTop": 20,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GlobalSecondaryIndexComponent
|
<MaterializedViewComponent
|
||||||
collection={
|
collection={
|
||||||
{
|
{
|
||||||
"analyticalStorageTtl": [Function],
|
"analyticalStorageTtl": [Function],
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import Explorer from "../Explorer";
|
|||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||||
|
|
||||||
// TODO: this does not seem to be used. Remove?
|
|
||||||
export class DataSamplesUtil {
|
export class DataSamplesUtil {
|
||||||
private static readonly DialogTitle = "Create Sample Container";
|
private static readonly DialogTitle = "Create Sample Container";
|
||||||
constructor(private container: Explorer) {}
|
constructor(private container: Explorer) {}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Notebook container related stuff
|
* Notebook container related stuff
|
||||||
*/
|
*/
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
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 { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||||
@@ -19,7 +19,7 @@ export class NotebookContainerClient {
|
|||||||
private clearReconnectionAttemptMessage? = () => {};
|
private clearReconnectionAttemptMessage? = () => {};
|
||||||
private isResettingWorkspace: boolean;
|
private isResettingWorkspace: boolean;
|
||||||
private phoenixClient: PhoenixClient;
|
private phoenixClient: PhoenixClient;
|
||||||
private retryOptions: Options;
|
private retryOptions: promiseRetry.Options;
|
||||||
private scheduleTimerId: NodeJS.Timeout;
|
private scheduleTimerId: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(private onConnectionLost: () => void) {
|
constructor(private onConnectionLost: () => void) {
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ import {
|
|||||||
isVectorSearchEnabled,
|
isVectorSearchEnabled,
|
||||||
} from "Utils/CapabilityUtils";
|
} from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
|
import "../../Controls/ThroughputInput/ThroughputInput.less";
|
||||||
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
@@ -331,8 +331,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder="Type a new database id"
|
placeholder="Type a new database id"
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
@@ -439,8 +439,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder={`e.g., ${getCollectionName()}1`}
|
placeholder={`e.g., ${getCollectionName()}1`}
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
id="newDatabaseId"
|
id="newDatabaseId"
|
||||||
name="newDatabaseId"
|
name="newDatabaseId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
placeholder="Type a new database id"
|
placeholder="Type a new database id"
|
||||||
required={true}
|
required={true}
|
||||||
size={40}
|
size={40}
|
||||||
@@ -178,7 +178,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
id="collectionId"
|
id="collectionId"
|
||||||
name="collectionId"
|
name="collectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
placeholder="e.g., Container1"
|
placeholder="e.g., Container1"
|
||||||
required={true}
|
required={true}
|
||||||
size={40}
|
size={40}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
@@ -205,8 +204,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
size={40}
|
size={40}
|
||||||
aria-label={databaseIdLabel}
|
aria-label={databaseIdLabel}
|
||||||
placeholder={databaseIdPlaceHolder}
|
placeholder={databaseIdPlaceHolder}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
data-lpignore={true}
|
data-lpignore={true}
|
||||||
id="database-id"
|
id="database-id"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
placeholder="Type a new database id"
|
placeholder="Type a new database id"
|
||||||
size={40}
|
size={40}
|
||||||
styles={
|
styles={
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { shallow, ShallowWrapper } from "enzyme";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
AddGlobalSecondaryIndexPanel,
|
|
||||||
AddGlobalSecondaryIndexPanelProps,
|
|
||||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
|
||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
const props: AddGlobalSecondaryIndexPanelProps = {
|
|
||||||
explorer: new Explorer(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("AddGlobalSecondaryIndexPanel", () => {
|
|
||||||
it("render default panel", () => {
|
|
||||||
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
|
|
||||||
<AddGlobalSecondaryIndexPanel {...props} />,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render form", () => {
|
|
||||||
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
|
|
||||||
<AddGlobalSecondaryIndexPanel {...props} />,
|
|
||||||
);
|
|
||||||
const form = wrapper.find("form").first();
|
|
||||||
expect(form).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -5,12 +5,12 @@ import React from "react";
|
|||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
export interface AdvancedComponentProps {
|
export interface AddMVAdvancedComponentProps {
|
||||||
useHashV1: boolean;
|
useHashV1: boolean;
|
||||||
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
|
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
}
|
}
|
||||||
export const AdvancedComponent = (props: AdvancedComponentProps): JSX.Element => {
|
export const AddMVAdvancedComponent = (props: AddMVAdvancedComponentProps): JSX.Element => {
|
||||||
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
|
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
|
||||||
|
|
||||||
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
|
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
|
||||||
@@ -23,7 +23,7 @@ export const AdvancedComponent = (props: AdvancedComponentProps): JSX.Element =>
|
|||||||
title="Advanced"
|
title="Advanced"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddGlobalSecondaryIndexPaneAdvancedSection);
|
TelemetryProcessor.traceOpen(Action.ExpandAddMaterializedViewPaneAdvancedSection);
|
||||||
scrollToSection("collapsibleAdvancedSectionContent");
|
scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
|
|
||||||
export interface AnalyticalStoreComponentProps {
|
export interface AddMVAnalyticalStoreComponentProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
enableAnalyticalStore: boolean;
|
enableAnalyticalStore: boolean;
|
||||||
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
|
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
export const AnalyticalStoreComponent = (props: AnalyticalStoreComponentProps): JSX.Element => {
|
export const AddMVAnalyticalStoreComponent = (props: AddMVAnalyticalStoreComponentProps): JSX.Element => {
|
||||||
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
|
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
|
||||||
|
|
||||||
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
|
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
|
||||||
@@ -5,13 +5,13 @@ import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullT
|
|||||||
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export interface FullTextSearchComponentProps {
|
export interface AddMVFullTextSearchComponentProps {
|
||||||
fullTextPolicy: FullTextPolicy;
|
fullTextPolicy: FullTextPolicy;
|
||||||
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
|
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
|
||||||
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
|
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
|
||||||
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
export const FullTextSearchComponent = (props: FullTextSearchComponentProps): JSX.Element => {
|
export const AddMVFullTextSearchComponent = (props: AddMVFullTextSearchComponentProps): JSX.Element => {
|
||||||
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
|
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export interface PartitionKeyComponentProps {
|
export interface AddMVPartitionKeyComponentProps {
|
||||||
partitionKey?: string;
|
partitionKey?: string;
|
||||||
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
|
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
|
||||||
subPartitionKeys: string[];
|
subPartitionKeys: string[];
|
||||||
@@ -15,7 +15,7 @@ export interface PartitionKeyComponentProps {
|
|||||||
useHashV1: boolean;
|
useHashV1: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PartitionKeyComponent = (props: PartitionKeyComponentProps): JSX.Element => {
|
export const AddMVPartitionKeyComponent = (props: AddMVPartitionKeyComponentProps): JSX.Element => {
|
||||||
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
|
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
|
||||||
|
|
||||||
const partitionKeyValueOnChange = (value: string): void => {
|
const partitionKeyValueOnChange = (value: string): void => {
|
||||||
@@ -50,7 +50,7 @@ export const PartitionKeyComponent = (props: PartitionKeyComponentProps): JSX.El
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="addGlobalSecondaryIndex-partitionKeyValue"
|
id="addmaterializedView-partitionKeyValue"
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
@@ -77,8 +77,8 @@ export const PartitionKeyComponent = (props: PartitionKeyComponentProps): JSX.El
|
|||||||
></div>
|
></div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="addGlobalSecondaryIndex-partitionKeyValue"
|
id="addMaterializedView-partitionKeyValue"
|
||||||
key={`addGlobalSecondaryIndex-partitionKeyValue_${subPartitionKeyIndex}`}
|
key={`addMaterializedView-partitionKeyValue_${subPartitionKeyIndex}`}
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
@@ -6,25 +6,25 @@ import React from "react";
|
|||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
|
|
||||||
export interface ThroughputComponentProps {
|
export interface AddMVThroughputComponentProps {
|
||||||
enableDedicatedThroughput: boolean;
|
enableDedicatedThroughput: boolean;
|
||||||
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
|
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isSelectedSourceContainerSharedThroughput: () => boolean;
|
isSelectedSourceContainerSharedThroughput: () => boolean;
|
||||||
showCollectionThroughputInput: () => boolean;
|
showCollectionThroughputInput: () => boolean;
|
||||||
globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void;
|
materializedViewThroughputOnChange: (materializedViewThroughputValue: number) => void;
|
||||||
isGlobalSecondaryIndexAutoscaleOnChange: (isGlobalSecondaryIndexAutoscaleValue: boolean) => void;
|
isMaterializedViewAutoscaleOnChange: (isMaterializedViewAutoscaleValue: boolean) => void;
|
||||||
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
|
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Element => {
|
export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
enableDedicatedThroughput,
|
enableDedicatedThroughput,
|
||||||
setEnabledDedicatedThroughput,
|
setEnabledDedicatedThroughput,
|
||||||
isSelectedSourceContainerSharedThroughput,
|
isSelectedSourceContainerSharedThroughput,
|
||||||
showCollectionThroughputInput,
|
showCollectionThroughputInput,
|
||||||
globalSecondaryIndexThroughputOnChange,
|
materializedViewThroughputOnChange,
|
||||||
isGlobalSecondaryIndexAutoscaleOnChange,
|
isMaterializedViewAutoscaleOnChange,
|
||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
isCostAknowledgedOnChange,
|
isCostAknowledgedOnChange,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -53,10 +53,10 @@ export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Elemen
|
|||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={isFreeTierAccount()}
|
||||||
isQuickstart={false}
|
isQuickstart={false}
|
||||||
setThroughputValue={(throughput: number) => {
|
setThroughputValue={(throughput: number) => {
|
||||||
globalSecondaryIndexThroughputOnChange(throughput);
|
materializedViewThroughputOnChange(throughput);
|
||||||
}}
|
}}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => {
|
setIsAutoscale={(isAutoscale: boolean) => {
|
||||||
isGlobalSecondaryIndexAutoscaleOnChange(isAutoscale);
|
isMaterializedViewAutoscaleOnChange(isAutoscale);
|
||||||
}}
|
}}
|
||||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
|
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
|
||||||
setIsThroughputCapExceeded(isThroughputCapExceeded);
|
setIsThroughputCapExceeded(isThroughputCapExceeded);
|
||||||
@@ -3,12 +3,12 @@ import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectio
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
|
||||||
export interface UniqueKeysComponentProps {
|
export interface AddMVUniqueKeysComponentProps {
|
||||||
uniqueKeys: string[];
|
uniqueKeys: string[];
|
||||||
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UniqueKeysComponent = (props: UniqueKeysComponentProps): JSX.Element => {
|
export const AddMVUniqueKeysComponent = (props: AddMVUniqueKeysComponentProps): JSX.Element => {
|
||||||
const { uniqueKeys, setUniqueKeys } = props;
|
const { uniqueKeys, setUniqueKeys } = props;
|
||||||
|
|
||||||
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
|
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export interface VectorSearchComponentProps {
|
export interface AddMVVectorSearchComponentProps {
|
||||||
vectorEmbeddingPolicy: VectorEmbedding[];
|
vectorEmbeddingPolicy: VectorEmbedding[];
|
||||||
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
|
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
|
||||||
vectorIndexingPolicy: VectorIndex[];
|
vectorIndexingPolicy: VectorIndex[];
|
||||||
@@ -16,7 +16,7 @@ export interface VectorSearchComponentProps {
|
|||||||
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
|
export const AddMVVectorSearchComponent = (props: AddMVVectorSearchComponentProps): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
setVectorEmbeddingPolicy,
|
setVectorEmbeddingPolicy,
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { shallow, ShallowWrapper } from "enzyme";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import {
|
||||||
|
AddMaterializedViewPanel,
|
||||||
|
AddMaterializedViewPanelProps,
|
||||||
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
const props: AddMaterializedViewPanelProps = {
|
||||||
|
explorer: new Explorer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("AddMaterializedViewPanel", () => {
|
||||||
|
it("render default panel", () => {
|
||||||
|
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
||||||
|
<AddMaterializedViewPanel {...props} />,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render form", () => {
|
||||||
|
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
||||||
|
<AddMaterializedViewPanel {...props} />,
|
||||||
|
);
|
||||||
|
const form = wrapper.find("form").first();
|
||||||
|
expect(form).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
TooltipHost,
|
TooltipHost,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { createGlobalSecondaryIndex } from "Common/dataAccess/createMaterializedView";
|
import { createMaterializedView } from "Common/dataAccess/createMaterializedView";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
@@ -29,14 +29,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
chooseSourceContainerStyle,
|
chooseSourceContainerStyle,
|
||||||
chooseSourceContainerStyles,
|
chooseSourceContainerStyles,
|
||||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles";
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanelStyles";
|
||||||
import { AdvancedComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent";
|
import { AddMVAdvancedComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent";
|
||||||
import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent";
|
import { AddMVAnalyticalStoreComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent";
|
||||||
import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent";
|
import { AddMVFullTextSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent";
|
||||||
import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent";
|
import { AddMVPartitionKeyComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVPartitionKeyComponent";
|
||||||
import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent";
|
import { AddMVThroughputComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent";
|
||||||
import { UniqueKeysComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent";
|
import { AddMVUniqueKeysComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVUniqueKeysComponent";
|
||||||
import { VectorSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent";
|
import { AddMVVectorSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVVectorSearchComponent";
|
||||||
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
|
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
|
||||||
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
|
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
|
||||||
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
||||||
@@ -48,18 +48,17 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
|
|
||||||
export interface AddGlobalSecondaryIndexPanelProps {
|
export interface AddMaterializedViewPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
sourceContainer?: Collection;
|
sourceContainer?: Collection;
|
||||||
}
|
}
|
||||||
export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanelProps): JSX.Element => {
|
export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): JSX.Element => {
|
||||||
const { explorer, sourceContainer } = props;
|
const { explorer, sourceContainer } = props;
|
||||||
|
|
||||||
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
|
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
|
||||||
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
|
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
|
||||||
const [globalSecondaryIndexId, setGlobalSecondaryIndexId] = useState<string>();
|
const [materializedViewId, setMaterializedViewId] = useState<string>();
|
||||||
const [definition, setDefinition] = useState<string>();
|
const [definition, setDefinition] = useState<string>();
|
||||||
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
|
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
|
||||||
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
|
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
|
||||||
@@ -88,13 +87,13 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
});
|
});
|
||||||
|
|
||||||
database.collections().forEach((collection: Collection) => {
|
database.collections().forEach((collection: Collection) => {
|
||||||
const isGlobalSecondaryIndex: boolean = !!collection.materializedViewDefinition();
|
const isMaterializedView: boolean = !!collection.materializedViewDefinition();
|
||||||
sourceContainerOptions.push({
|
sourceContainerOptions.push({
|
||||||
key: collection.rid,
|
key: collection.rid,
|
||||||
text: collection.id(),
|
text: collection.id(),
|
||||||
disabled: isGlobalSecondaryIndex,
|
disabled: isMaterializedView,
|
||||||
...(isGlobalSecondaryIndex && {
|
...(isMaterializedView && {
|
||||||
title: "This is a global secondary index.",
|
title: "This is a materialized view.",
|
||||||
}),
|
}),
|
||||||
data: collection,
|
data: collection,
|
||||||
});
|
});
|
||||||
@@ -108,16 +107,16 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
scrollToSection("panelContainer");
|
scrollToSection("panelContainer");
|
||||||
}, [errorMessage]);
|
}, [errorMessage]);
|
||||||
|
|
||||||
let globalSecondaryIndexThroughput: number;
|
let materializedViewThroughput: number;
|
||||||
let isGlobalSecondaryIndexAutoscale: boolean;
|
let isMaterializedViewAutoscale: boolean;
|
||||||
let isCostAcknowledged: boolean;
|
let isCostAcknowledged: boolean;
|
||||||
|
|
||||||
const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => {
|
const materializedViewThroughputOnChange = (materializedViewThroughputValue: number): void => {
|
||||||
globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue;
|
materializedViewThroughput = materializedViewThroughputValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isGlobalSecondaryIndexAutoscaleOnChange = (isGlobalSecondaryIndexAutoscaleValue: boolean): void => {
|
const isMaterializedViewAutoscaleOnChange = (isMaterializedViewAutoscaleValue: boolean): void => {
|
||||||
isGlobalSecondaryIndexAutoscale = isGlobalSecondaryIndexAutoscaleValue;
|
isMaterializedViewAutoscale = isMaterializedViewAutoscaleValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
|
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
|
||||||
@@ -177,15 +176,15 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
if (materializedViewThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||||
const errorMessage = isGlobalSecondaryIndexAutoscale
|
const errorMessage = isMaterializedViewAutoscale
|
||||||
? "Please acknowledge the estimated monthly spend."
|
? "Please acknowledge the estimated monthly spend."
|
||||||
: "Please acknowledge the estimated daily spend.";
|
: "Please acknowledge the estimated daily spend.";
|
||||||
setErrorMessage(errorMessage);
|
setErrorMessage(errorMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalSecondaryIndexThroughput > CollectionCreation.MaxRUPerPartition) {
|
if (materializedViewThroughput > CollectionCreation.MaxRUPerPartition) {
|
||||||
setErrorMessage("Unsharded collections support up to 10,000 RUs");
|
setErrorMessage("Unsharded collections support up to 10,000 RUs");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -212,10 +211,10 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalSecondaryIdTrimmed: string = globalSecondaryIndexId.trim();
|
const materializedViewIdTrimmed: string = materializedViewId.trim();
|
||||||
|
|
||||||
const globalSecondaryIndexDefinition: DataModels.MaterializedViewDefinition = {
|
const materializedViewDefinition: DataModels.MaterializedViewDefinition = {
|
||||||
sourceCollectionId: selectedSourceContainer.id(),
|
sourceCollectionId: sourceContainer.id(),
|
||||||
definition: definition,
|
definition: definition,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,9 +253,9 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
shared: isSelectedSourceContainerSharedThroughput(),
|
shared: isSelectedSourceContainerSharedThroughput(),
|
||||||
},
|
},
|
||||||
collection: {
|
collection: {
|
||||||
id: globalSecondaryIdTrimmed,
|
id: materializedViewIdTrimmed,
|
||||||
throughput: globalSecondaryIndexThroughput,
|
throughput: materializedViewThroughput,
|
||||||
isAutoscale: isGlobalSecondaryIndexAutoscale,
|
isAutoscale: isMaterializedViewAutoscale,
|
||||||
partitionKeyPaths,
|
partitionKeyPaths,
|
||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithDedicatedThroughput: enableDedicatedThroughput,
|
collectionWithDedicatedThroughput: enableDedicatedThroughput,
|
||||||
@@ -272,16 +271,16 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
let autoPilotMaxThroughput: number;
|
let autoPilotMaxThroughput: number;
|
||||||
|
|
||||||
if (!databaseLevelThroughput) {
|
if (!databaseLevelThroughput) {
|
||||||
if (isGlobalSecondaryIndexAutoscale) {
|
if (isMaterializedViewAutoscale) {
|
||||||
autoPilotMaxThroughput = globalSecondaryIndexThroughput;
|
autoPilotMaxThroughput = materializedViewThroughput;
|
||||||
} else {
|
} else {
|
||||||
offerThroughput = globalSecondaryIndexThroughput;
|
offerThroughput = materializedViewThroughput;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = {
|
const createMaterializedViewParams: DataModels.CreateMaterializedViewsParams = {
|
||||||
materializedViewId: globalSecondaryIdTrimmed,
|
materializedViewId: materializedViewIdTrimmed,
|
||||||
materializedViewDefinition: globalSecondaryIndexDefinition,
|
materializedViewDefinition: materializedViewDefinition,
|
||||||
databaseId: selectedSourceContainer.databaseId,
|
databaseId: selectedSourceContainer.databaseId,
|
||||||
databaseLevelThroughput: databaseLevelThroughput,
|
databaseLevelThroughput: databaseLevelThroughput,
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
@@ -297,23 +296,23 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createGlobalSecondaryIndex(createGlobalSecondaryIndexParams);
|
await createMaterializedView(createMaterializedViewParams);
|
||||||
await explorer.refreshAllDatabases();
|
await explorer.refreshAllDatabases();
|
||||||
TelemetryProcessor.traceSuccess(Action.CreateGlobalSecondaryIndex, telemetryData, startKey);
|
TelemetryProcessor.traceSuccess(Action.CreateMaterializedView, telemetryData, startKey);
|
||||||
useSidePanel.getState().closeSidePanel();
|
useSidePanel.getState().closeSidePanel();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage: string = getErrorMessage(error);
|
const errorMessage: string = getErrorMessage(error);
|
||||||
setErrorMessage(errorMessage);
|
setErrorMessage(errorMessage);
|
||||||
setShowErrorDetails(true);
|
setShowErrorDetails(true);
|
||||||
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
||||||
TelemetryProcessor.traceFailure(Action.CreateGlobalSecondaryIndex, failureTelemetryData, startKey);
|
TelemetryProcessor.traceFailure(Action.CreateMaterializedView, failureTelemetryData, startKey);
|
||||||
} finally {
|
} finally {
|
||||||
setIsExecuting(false);
|
setIsExecuting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="panelFormWrapper" id="panelGlobalSecondaryIndex" onSubmit={submit}>
|
<form className="panelFormWrapper" id="panelMaterializedView" onSubmit={submit}>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
||||||
)}
|
)}
|
||||||
@@ -328,7 +327,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
placeholder="Choose source container"
|
placeholder="Choose source container"
|
||||||
options={sourceContainerOptions}
|
options={sourceContainerOptions}
|
||||||
defaultSelectedKey={selectedSourceContainer?.rid}
|
defaultSelectedKey={sourceContainer?.rid}
|
||||||
styles={chooseSourceContainerStyles()}
|
styles={chooseSourceContainerStyles()}
|
||||||
style={chooseSourceContainerStyle()}
|
style={chooseSourceContainerStyle()}
|
||||||
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
|
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
|
||||||
@@ -337,27 +336,27 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
Global secondary index container id
|
View container id
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<input
|
<input
|
||||||
id="globalSecondaryIndexId"
|
id="materializedViewId"
|
||||||
type="text"
|
type="text"
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder={`e.g., indexbyEmailId`}
|
placeholder={`e.g., viewByEmailId`}
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
value={globalSecondaryIndexId}
|
value={materializedViewId}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setGlobalSecondaryIndexId(event.target.value)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setMaterializedViewId(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
Global secondary index definition
|
Materialized View Definition
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
@@ -366,7 +365,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||||
target="blank"
|
target="blank"
|
||||||
>
|
>
|
||||||
Learn more about defining global secondary indexes.
|
Learn more about defining materialized views.
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -374,7 +373,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
<input
|
<input
|
||||||
id="globalSecondaryIndexDefinition"
|
id="materializedViewDefinition"
|
||||||
type="text"
|
type="text"
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
@@ -385,27 +384,27 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
value={definition || ""}
|
value={definition || ""}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<PartitionKeyComponent
|
<AddMVPartitionKeyComponent
|
||||||
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
|
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
|
||||||
/>
|
/>
|
||||||
<ThroughputComponent
|
<AddMVThroughputComponent
|
||||||
{...{
|
{...{
|
||||||
enableDedicatedThroughput,
|
enableDedicatedThroughput,
|
||||||
setEnabledDedicatedThroughput,
|
setEnabledDedicatedThroughput,
|
||||||
isSelectedSourceContainerSharedThroughput,
|
isSelectedSourceContainerSharedThroughput,
|
||||||
showCollectionThroughputInput,
|
showCollectionThroughputInput,
|
||||||
globalSecondaryIndexThroughputOnChange,
|
materializedViewThroughputOnChange,
|
||||||
isGlobalSecondaryIndexAutoscaleOnChange,
|
isMaterializedViewAutoscaleOnChange,
|
||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
isCostAknowledgedOnChange,
|
isCostAknowledgedOnChange,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<UniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
|
<AddMVUniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
{shouldShowAnalyticalStoreOptions() && (
|
||||||
<AnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
|
<AddMVAnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
|
||||||
)}
|
)}
|
||||||
{showVectorSearchParameters() && (
|
{showVectorSearchParameters() && (
|
||||||
<VectorSearchComponent
|
<AddMVVectorSearchComponent
|
||||||
{...{
|
{...{
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
setVectorEmbeddingPolicy,
|
setVectorEmbeddingPolicy,
|
||||||
@@ -417,11 +416,11 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showFullTextSearchParameters() && (
|
{showFullTextSearchParameters() && (
|
||||||
<FullTextSearchComponent
|
<AddMVFullTextSearchComponent
|
||||||
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
<AddMVAdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
exports[`AddMaterializedViewPanel render default panel 1`] = `
|
||||||
<form
|
<form
|
||||||
className="panelFormWrapper"
|
className="panelFormWrapper"
|
||||||
id="panelGlobalSecondaryIndex"
|
id="panelMaterializedView"
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -67,17 +67,17 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
className="panelTextBold"
|
className="panelTextBold"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Global secondary index container id
|
View container id
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<input
|
<input
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
id="globalSecondaryIndexId"
|
id="materializedViewId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||||
placeholder="e.g., indexbyEmailId"
|
placeholder="e.g., viewByEmailId"
|
||||||
required={true}
|
required={true}
|
||||||
size={40}
|
size={40}
|
||||||
title="May not end with space nor contain characters '\\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\\' '/' '#' '?'"
|
||||||
@@ -95,7 +95,7 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
className="panelTextBold"
|
className="panelTextBold"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Global secondary index definition
|
Materialized View Definition
|
||||||
</Text>
|
</Text>
|
||||||
<StyledTooltipHostBase
|
<StyledTooltipHostBase
|
||||||
content={
|
content={
|
||||||
@@ -103,7 +103,7 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||||
target="blank"
|
target="blank"
|
||||||
>
|
>
|
||||||
Learn more about defining global secondary indexes.
|
Learn more about defining materialized views.
|
||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
}
|
}
|
||||||
directionalHint={4}
|
directionalHint={4}
|
||||||
@@ -120,7 +120,7 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
aria-required={true}
|
aria-required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
id="globalSecondaryIndexDefinition"
|
id="materializedViewDefinition"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
placeholder="SELECT c.email, c.accountId FROM c"
|
placeholder="SELECT c.email, c.accountId FROM c"
|
||||||
required={true}
|
required={true}
|
||||||
@@ -128,26 +128,26 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<PartitionKeyComponent
|
<AddMVPartitionKeyComponent
|
||||||
partitionKey=""
|
partitionKey=""
|
||||||
setPartitionKey={[Function]}
|
setPartitionKey={[Function]}
|
||||||
setSubPartitionKeys={[Function]}
|
setSubPartitionKeys={[Function]}
|
||||||
subPartitionKeys={[]}
|
subPartitionKeys={[]}
|
||||||
/>
|
/>
|
||||||
<ThroughputComponent
|
<AddMVThroughputComponent
|
||||||
globalSecondaryIndexThroughputOnChange={[Function]}
|
|
||||||
isCostAknowledgedOnChange={[Function]}
|
isCostAknowledgedOnChange={[Function]}
|
||||||
isGlobalSecondaryIndexAutoscaleOnChange={[Function]}
|
isMaterializedViewAutoscaleOnChange={[Function]}
|
||||||
isSelectedSourceContainerSharedThroughput={[Function]}
|
isSelectedSourceContainerSharedThroughput={[Function]}
|
||||||
|
materializedViewThroughputOnChange={[Function]}
|
||||||
setEnabledDedicatedThroughput={[Function]}
|
setEnabledDedicatedThroughput={[Function]}
|
||||||
setIsThroughputCapExceeded={[Function]}
|
setIsThroughputCapExceeded={[Function]}
|
||||||
showCollectionThroughputInput={[Function]}
|
showCollectionThroughputInput={[Function]}
|
||||||
/>
|
/>
|
||||||
<UniqueKeysComponent
|
<AddMVUniqueKeysComponent
|
||||||
setUniqueKeys={[Function]}
|
setUniqueKeys={[Function]}
|
||||||
uniqueKeys={[]}
|
uniqueKeys={[]}
|
||||||
/>
|
/>
|
||||||
<AnalyticalStoreComponent
|
<AddMVAnalyticalStoreComponent
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
@@ -177,7 +177,7 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
}
|
}
|
||||||
setEnableAnalyticalStore={[Function]}
|
setEnableAnalyticalStore={[Function]}
|
||||||
/>
|
/>
|
||||||
<AdvancedComponent
|
<AddMVAdvancedComponent
|
||||||
setSubPartitionKeys={[Function]}
|
setSubPartitionKeys={[Function]}
|
||||||
setUseHashV1={[Function]}
|
setUseHashV1={[Function]}
|
||||||
/>
|
/>
|
||||||
@@ -7,7 +7,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
@@ -203,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
styles={getTextFieldStyles()}
|
styles={getTextFieldStyles()}
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Type a new keyspace id"
|
placeholder="Type a new keyspace id"
|
||||||
size={40}
|
size={40}
|
||||||
value={newKeyspaceId}
|
value={newKeyspaceId}
|
||||||
@@ -293,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
required={true}
|
required={true}
|
||||||
ariaLabel="addCollection-table Id Create table"
|
ariaLabel="addCollection-table Id Create table"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||||
placeholder="Enter table Id"
|
placeholder="Enter table Id"
|
||||||
size={20}
|
size={20}
|
||||||
value={tableId}
|
value={tableId}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
|||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
@@ -236,8 +235,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder={`e.g., ${getCollectionName()}1`}
|
placeholder={`e.g., ${getCollectionName()}1`}
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
ChoiceGroup,
|
ChoiceGroup,
|
||||||
DefaultButton,
|
DefaultButton,
|
||||||
Dropdown,
|
|
||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
IDropdownOption,
|
|
||||||
ISpinButtonStyles,
|
ISpinButtonStyles,
|
||||||
IToggleStyles,
|
IToggleStyles,
|
||||||
Position,
|
Position,
|
||||||
@@ -23,15 +21,7 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { deleteAllStates } from "Shared/AppStatePersistenceUtility";
|
||||||
import {
|
|
||||||
AppStateComponentNames,
|
|
||||||
deleteAllStates,
|
|
||||||
deleteState,
|
|
||||||
hasState,
|
|
||||||
loadState,
|
|
||||||
saveState,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
|
||||||
import {
|
import {
|
||||||
DefaultRUThreshold,
|
DefaultRUThreshold,
|
||||||
LocalStorageUtility,
|
LocalStorageUtility,
|
||||||
@@ -47,7 +37,6 @@ import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
|||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
@@ -154,17 +143,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled)
|
? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled)
|
||||||
: "false",
|
: "false",
|
||||||
);
|
);
|
||||||
const [selectedRegionalEndpoint, setSelectedRegionalEndpoint] = useState<string>(
|
|
||||||
hasState({
|
|
||||||
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
|
|
||||||
globalAccountName: userContext.databaseAccount?.name,
|
|
||||||
})
|
|
||||||
? (loadState({
|
|
||||||
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
|
|
||||||
globalAccountName: userContext.databaseAccount?.name,
|
|
||||||
}) as string)
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
const [retryAttempts, setRetryAttempts] = useState<number>(
|
const [retryAttempts, setRetryAttempts] = useState<number>(
|
||||||
LocalStorageUtility.hasItem(StorageKey.RetryAttempts)
|
LocalStorageUtility.hasItem(StorageKey.RetryAttempts)
|
||||||
? LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts)
|
? LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts)
|
||||||
@@ -211,44 +189,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
configContext.platform !== Platform.Fabric &&
|
configContext.platform !== Platform.Fabric &&
|
||||||
!isEmulator;
|
!isEmulator;
|
||||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
|
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
|
||||||
|
|
||||||
const uniqueAccountRegions = new Set<string>();
|
|
||||||
const regionOptions: IDropdownOption[] = [];
|
|
||||||
regionOptions.push({
|
|
||||||
key: userContext?.databaseAccount?.properties?.documentEndpoint,
|
|
||||||
text: `Global (Default)`,
|
|
||||||
data: {
|
|
||||||
isGlobal: true,
|
|
||||||
writeEnabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
userContext?.databaseAccount?.properties?.writeLocations?.forEach((loc) => {
|
|
||||||
if (!uniqueAccountRegions.has(loc.locationName)) {
|
|
||||||
uniqueAccountRegions.add(loc.locationName);
|
|
||||||
regionOptions.push({
|
|
||||||
key: loc.documentEndpoint,
|
|
||||||
text: `${loc.locationName} (Read/Write)`,
|
|
||||||
data: {
|
|
||||||
isGlobal: false,
|
|
||||||
writeEnabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
userContext?.databaseAccount?.properties?.readLocations?.forEach((loc) => {
|
|
||||||
if (!uniqueAccountRegions.has(loc.locationName)) {
|
|
||||||
uniqueAccountRegions.add(loc.locationName);
|
|
||||||
regionOptions.push({
|
|
||||||
key: loc.documentEndpoint,
|
|
||||||
text: `${loc.locationName} (Read)`,
|
|
||||||
data: {
|
|
||||||
isGlobal: false,
|
|
||||||
writeEnabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldShowCopilotSampleDBOption =
|
const shouldShowCopilotSampleDBOption =
|
||||||
userContext.apiType === "SQL" &&
|
userContext.apiType === "SQL" &&
|
||||||
useQueryCopilot.getState().copilotEnabled &&
|
useQueryCopilot.getState().copilotEnabled &&
|
||||||
@@ -334,46 +274,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedRegionalEndpoint = loadState({
|
|
||||||
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
|
|
||||||
globalAccountName: userContext.databaseAccount?.name,
|
|
||||||
}) as string;
|
|
||||||
const selectedRegionIsGlobal =
|
|
||||||
selectedRegionalEndpoint === userContext?.databaseAccount?.properties?.documentEndpoint;
|
|
||||||
if (selectedRegionIsGlobal && storedRegionalEndpoint) {
|
|
||||||
deleteState({
|
|
||||||
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
|
|
||||||
globalAccountName: userContext.databaseAccount?.name,
|
|
||||||
});
|
|
||||||
updateUserContext({
|
|
||||||
selectedRegionalEndpoint: undefined,
|
|
||||||
writeEnabledInSelectedRegion: true,
|
|
||||||
refreshCosmosClient: true,
|
|
||||||
});
|
|
||||||
useClientWriteEnabled.setState({ clientWriteEnabled: true });
|
|
||||||
} else if (
|
|
||||||
selectedRegionalEndpoint &&
|
|
||||||
!selectedRegionIsGlobal &&
|
|
||||||
selectedRegionalEndpoint !== storedRegionalEndpoint
|
|
||||||
) {
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
|
|
||||||
globalAccountName: userContext.databaseAccount?.name,
|
|
||||||
},
|
|
||||||
selectedRegionalEndpoint,
|
|
||||||
);
|
|
||||||
const validWriteEndpoint = userContext.databaseAccount?.properties?.writeLocations?.find(
|
|
||||||
(loc) => loc.documentEndpoint === selectedRegionalEndpoint,
|
|
||||||
);
|
|
||||||
updateUserContext({
|
|
||||||
selectedRegionalEndpoint: selectedRegionalEndpoint,
|
|
||||||
writeEnabledInSelectedRegion: !!validWriteEndpoint,
|
|
||||||
refreshCosmosClient: true,
|
|
||||||
});
|
|
||||||
useClientWriteEnabled.setState({ clientWriteEnabled: !!validWriteEndpoint });
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
||||||
@@ -523,10 +423,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
setDefaultQueryResultsView(option.key as SplitterDirection);
|
setDefaultQueryResultsView(option.key as SplitterDirection);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnSelectedRegionOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IDropdownOption): void => {
|
|
||||||
setSelectedRegionalEndpoint(option.key as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
|
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
|
||||||
const retryAttempts = Number(newValue);
|
const retryAttempts = Number(newValue);
|
||||||
if (!isNaN(retryAttempts)) {
|
if (!isNaN(retryAttempts)) {
|
||||||
@@ -687,39 +583,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
|
|
||||||
<AccordionItem value="3">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Region Selection</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Changes region the Cosmos Client uses to access account.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={styles.subHeader}>Select Region</span>
|
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
|
||||||
Changes the account endpoint used to perform client operations.
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
|
||||||
<Dropdown
|
|
||||||
placeholder={
|
|
||||||
selectedRegionalEndpoint
|
|
||||||
? regionOptions.find((option) => option.key === selectedRegionalEndpoint)?.text
|
|
||||||
: regionOptions[0]?.text
|
|
||||||
}
|
|
||||||
onChange={handleOnSelectedRegionOptionChange}
|
|
||||||
options={regionOptions}
|
|
||||||
styles={{ root: { marginBottom: "10px" } }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
)}
|
|
||||||
{userContext.apiType === "SQL" && !isEmulator && (
|
{userContext.apiType === "SQL" && !isEmulator && (
|
||||||
<>
|
<>
|
||||||
<AccordionItem value="4">
|
<AccordionItem value="3">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Query Timeout</div>
|
<div className={styles.header}>Query Timeout</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -760,7 +626,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="5">
|
<AccordionItem value="4">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>RU Limit</div>
|
<div className={styles.header}>RU Limit</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -794,7 +660,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="6">
|
<AccordionItem value="5">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Default Query Results View</div>
|
<div className={styles.header}>Default Query Results View</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -815,9 +681,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showRetrySettings && (
|
{showRetrySettings && (
|
||||||
<AccordionItem value="7">
|
<AccordionItem value="6">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Retry Settings</div>
|
<div className={styles.header}>Retry Settings</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -890,7 +755,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{!isEmulator && (
|
{!isEmulator && (
|
||||||
<AccordionItem value="8">
|
<AccordionItem value="7">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Enable container pagination</div>
|
<div className={styles.header}>Enable container pagination</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -914,7 +779,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{shouldShowCrossPartitionOption && (
|
{shouldShowCrossPartitionOption && (
|
||||||
<AccordionItem value="9">
|
<AccordionItem value="8">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Enable cross-partition query</div>
|
<div className={styles.header}>Enable cross-partition query</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -939,7 +804,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{shouldShowParallelismOption && (
|
{shouldShowParallelismOption && (
|
||||||
<AccordionItem value="10">
|
<AccordionItem value="9">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Max degree of parallelism</div>
|
<div className={styles.header}>Max degree of parallelism</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -972,7 +837,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{shouldShowPriorityLevelOption && (
|
{shouldShowPriorityLevelOption && (
|
||||||
<AccordionItem value="11">
|
<AccordionItem value="10">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Priority Level</div>
|
<div className={styles.header}>Priority Level</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -995,7 +860,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{shouldShowGraphAutoVizOption && (
|
{shouldShowGraphAutoVizOption && (
|
||||||
<AccordionItem value="12">
|
<AccordionItem value="11">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Display Gremlin query results as: </div>
|
<div className={styles.header}>Display Gremlin query results as: </div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -1016,7 +881,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{shouldShowCopilotSampleDBOption && (
|
{shouldShowCopilotSampleDBOption && (
|
||||||
<AccordionItem value="13">
|
<AccordionItem value="12">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Enable sample database</div>
|
<div className={styles.header}>Enable sample database</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
@@ -1051,15 +916,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
"Clear History",
|
"Clear History",
|
||||||
undefined,
|
undefined,
|
||||||
"Are you sure you want to proceed?",
|
"Are you sure you want to proceed?",
|
||||||
() => {
|
() => deleteAllStates(),
|
||||||
deleteAllStates();
|
|
||||||
updateUserContext({
|
|
||||||
selectedRegionalEndpoint: undefined,
|
|
||||||
writeEnabledInSelectedRegion: true,
|
|
||||||
refreshCosmosClient: true,
|
|
||||||
});
|
|
||||||
useClientWriteEnabled.setState({ clientWriteEnabled: true });
|
|
||||||
},
|
|
||||||
"Cancel",
|
"Cancel",
|
||||||
undefined,
|
undefined,
|
||||||
<>
|
<>
|
||||||
@@ -1070,7 +927,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
<li>Reset your customized tab layout, including the splitter positions</li>
|
<li>Reset your customized tab layout, including the splitter positions</li>
|
||||||
<li>Erase your table column preferences, including any custom columns</li>
|
<li>Erase your table column preferences, including any custom columns</li>
|
||||||
<li>Clear your filter history</li>
|
<li>Clear your filter history</li>
|
||||||
<li>Reset region selection to global</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="4"
|
value="3"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -148,7 +148,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="5"
|
value="4"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -219,7 +219,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="6"
|
value="5"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -281,7 +281,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="7"
|
value="6"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -423,7 +423,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="8"
|
value="7"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -459,7 +459,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="9"
|
value="8"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -495,7 +495,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="10"
|
value="9"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -575,7 +575,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||||
>
|
>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="7"
|
value="6"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -717,7 +717,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="8"
|
value="7"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
@@ -753,7 +753,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value="12"
|
value="11"
|
||||||
>
|
>
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ import {
|
|||||||
SplitButton,
|
SplitButton,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
|
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
|
||||||
import { GlobalSecondaryIndexLabels } from "Common/Constants";
|
import { MaterializedViewsLabels } from "Common/Constants";
|
||||||
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
|
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
|
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
|
||||||
import {
|
import {
|
||||||
AddGlobalSecondaryIndexPanel,
|
AddMaterializedViewPanel,
|
||||||
AddGlobalSecondaryIndexPanelProps,
|
AddMaterializedViewPanelProps,
|
||||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||||
import { Tabs } from "Explorer/Tabs/Tabs";
|
import { Tabs } from "Explorer/Tabs/Tabs";
|
||||||
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||||
@@ -168,21 +168,21 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGlobalSecondaryIndexEnabled()) {
|
if (isMaterializedViewsEnabled()) {
|
||||||
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
|
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
||||||
explorer,
|
explorer,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
id: "new_materialized_view",
|
id: "new_materialized_view",
|
||||||
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
label: MaterializedViewsLabels.NewMaterializedView,
|
||||||
icon: <Add16Regular />,
|
icon: <Add16Regular />,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
MaterializedViewsLabels.NewMaterializedView,
|
||||||
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
|
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -340,18 +340,16 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
<>
|
<>
|
||||||
<div className={styles.floatingControlsContainer}>
|
<div className={styles.floatingControlsContainer}>
|
||||||
<div className={styles.floatingControls}>
|
<div className={styles.floatingControls}>
|
||||||
{!isFabricNative() && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
data-test="Sidebar/RefreshButton"
|
||||||
data-test="Sidebar/RefreshButton"
|
className={styles.floatingControlButton}
|
||||||
className={styles.floatingControlButton}
|
disabled={loading}
|
||||||
disabled={loading}
|
title="Refresh"
|
||||||
title="Refresh"
|
onClick={onRefreshClick}
|
||||||
onClick={onRefreshClick}
|
>
|
||||||
>
|
<ArrowSync12Regular />
|
||||||
<ArrowSync12Regular />
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.floatingControlButton}
|
className={styles.floatingControlButton}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
||||||
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
|
||||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
@@ -110,10 +108,12 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
|||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
|
// TODO Make this a11y copmliant: aria-label for icon
|
||||||
return (
|
return (
|
||||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
||||||
<div className={styles.buttonUpperPart}>{icon}</div>
|
<div className={styles.buttonUpperPart}>{icon}</div>
|
||||||
<div aria-label={title} className={styles.buttonLowerPart}>
|
<div className={styles.buttonLowerPart}>
|
||||||
<div>{title}</div>
|
<div>{title}</div>
|
||||||
<div>{description}</div>
|
<div>{description}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,8 +123,6 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
|||||||
|
|
||||||
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [openSampleDataImportDialog, setOpenSampleDataImportDialog] = React.useState(false);
|
|
||||||
|
|
||||||
const getSplashScreenButtons = (): JSX.Element => {
|
const getSplashScreenButtons = (): JSX.Element => {
|
||||||
const buttons: FabricHomeScreenButtonProps[] = [
|
const buttons: FabricHomeScreenButtonProps[] = [
|
||||||
{
|
{
|
||||||
@@ -140,13 +138,11 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
title: "Sample data",
|
title: "Sample data",
|
||||||
description: "Automatically load sample data in your database",
|
description: "Automatically load sample data in your database",
|
||||||
icon: <img src={CosmosDbBlackIcon} />,
|
icon: <img src={CosmosDbBlackIcon} />,
|
||||||
onClick: () => setOpenSampleDataImportDialog(true),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "App development",
|
title: "App development",
|
||||||
description: "Start here to use an SDK to build your apps",
|
description: "Start here to use an SDK to build your apps",
|
||||||
icon: <LinkMultipleRegular />,
|
icon: <LinkMultipleRegular />,
|
||||||
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -161,25 +157,17 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
|
|
||||||
const title = "Build your database";
|
const title = "Build your database";
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.homeContainer}>
|
||||||
<CosmosFluentProvider className={styles.homeContainer}>
|
<div className={styles.title} role="heading" aria-label={title}>
|
||||||
<SampleDataImportDialog
|
{title}
|
||||||
open={openSampleDataImportDialog}
|
</div>
|
||||||
setOpen={setOpenSampleDataImportDialog}
|
{getSplashScreenButtons()}
|
||||||
explorer={props.explorer}
|
<div className={styles.footer}>
|
||||||
databaseName={userContext.fabricContext?.databaseName}
|
Need help?{" "}
|
||||||
/>
|
<Link href="https://cosmos.azure.com/docs" target="_blank">
|
||||||
<div className={styles.title} role="heading" aria-label={title}>
|
Learn more <img src={LinkIcon} alt="Learn more" />
|
||||||
{title}
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{getSplashScreenButtons()}
|
</div>
|
||||||
<div className={styles.footer}>
|
|
||||||
Need help?{" "}
|
|
||||||
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
|
||||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</CosmosFluentProvider>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogBody,
|
|
||||||
DialogContent,
|
|
||||||
DialogSurface,
|
|
||||||
DialogTitle,
|
|
||||||
makeStyles,
|
|
||||||
Spinner,
|
|
||||||
tokens,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { checkContainerExists, createContainer, importData } from "Explorer/SplashScreen/SampleUtil";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
const SAMPLE_DATA_CONTAINER_NAME = "SampleData";
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
dialogContent: {
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: tokens.spacingVerticalL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This dialog:
|
|
||||||
* - creates a container
|
|
||||||
* - imports data into the container
|
|
||||||
* @param props
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const SampleDataImportDialog: React.FC<{
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
explorer: Explorer;
|
|
||||||
databaseName: string;
|
|
||||||
}> = (props) => {
|
|
||||||
const [status, setStatus] = useState<"idle" | "creating" | "importing" | "completed" | "error">("idle");
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
||||||
const containerName = SAMPLE_DATA_CONTAINER_NAME;
|
|
||||||
const [collection, setCollection] = useState<ViewModels.Collection>(undefined);
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset state when dialog opens
|
|
||||||
if (props.open) {
|
|
||||||
setStatus("idle");
|
|
||||||
setErrorMessage(undefined);
|
|
||||||
}
|
|
||||||
}, [props.open]);
|
|
||||||
|
|
||||||
const handleStartImport = async (): Promise<void> => {
|
|
||||||
setStatus("creating");
|
|
||||||
const databaseName = props.databaseName;
|
|
||||||
if (checkContainerExists(databaseName, containerName)) {
|
|
||||||
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
|
|
||||||
setStatus("error");
|
|
||||||
setErrorMessage(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let collection;
|
|
||||||
try {
|
|
||||||
collection = await createContainer(databaseName, containerName, props.explorer);
|
|
||||||
} catch (error) {
|
|
||||||
setStatus("error");
|
|
||||||
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setStatus("importing");
|
|
||||||
await importData(collection);
|
|
||||||
setCollection(collection);
|
|
||||||
setStatus("completed");
|
|
||||||
} catch (error) {
|
|
||||||
setStatus("error");
|
|
||||||
setErrorMessage(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleActionOnClick = () => {
|
|
||||||
switch (status) {
|
|
||||||
case "idle":
|
|
||||||
handleStartImport();
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
props.setOpen(false);
|
|
||||||
break;
|
|
||||||
case "creating":
|
|
||||||
case "importing":
|
|
||||||
props.setOpen(false);
|
|
||||||
break;
|
|
||||||
case "completed":
|
|
||||||
props.setOpen(false);
|
|
||||||
collection.openTab();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (status) {
|
|
||||||
case "idle":
|
|
||||||
return `Create a container "${containerName}" and import sample data into it. This may take a few minutes.`;
|
|
||||||
|
|
||||||
case "creating":
|
|
||||||
return <Spinner size="small" labelPosition="above" label={`Creating container "${containerName}"...`} />;
|
|
||||||
case "importing":
|
|
||||||
return <Spinner size="small" labelPosition="above" label={`Importing data into "${containerName}"...`} />;
|
|
||||||
case "completed":
|
|
||||||
return `Successfully created "${containerName}" with sample data.`;
|
|
||||||
case "error":
|
|
||||||
return (
|
|
||||||
<div style={{ color: "red" }}>
|
|
||||||
<div>Error: {errorMessage}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getButtonLabel = () => {
|
|
||||||
switch (status) {
|
|
||||||
case "idle":
|
|
||||||
return "Start";
|
|
||||||
case "creating":
|
|
||||||
case "importing":
|
|
||||||
return "Close";
|
|
||||||
case "completed":
|
|
||||||
return "Close";
|
|
||||||
case "error":
|
|
||||||
return "Close";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={props.open} onOpenChange={(event, data) => props.setOpen(data.open)}>
|
|
||||||
<DialogSurface>
|
|
||||||
<DialogBody>
|
|
||||||
<DialogTitle>Sample Data</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<div className={styles.dialogContent}>{renderContent()}</div>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
appearance="primary"
|
|
||||||
onClick={handleActionOnClick}
|
|
||||||
disabled={status === "creating" || status === "importing"}
|
|
||||||
>
|
|
||||||
{getButtonLabel()}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</DialogBody>
|
|
||||||
</DialogSurface>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public for unit tests
|
|
||||||
* @param databaseName
|
|
||||||
* @param containerName
|
|
||||||
* @param containerDatabases
|
|
||||||
*/
|
|
||||||
const hasContainer = (
|
|
||||||
databaseName: string,
|
|
||||||
containerName: string,
|
|
||||||
containerDatabases: ViewModels.Database[],
|
|
||||||
): boolean => {
|
|
||||||
const filteredDatabases = containerDatabases.filter((database) => database.id() === databaseName);
|
|
||||||
return (
|
|
||||||
filteredDatabases.length > 0 &&
|
|
||||||
filteredDatabases[0].collections().filter((collection) => collection.id() === containerName).length > 0
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkContainerExists = (databaseName: string, containerName: string) =>
|
|
||||||
hasContainer(databaseName, containerName, useDatabases.getState().databases);
|
|
||||||
|
|
||||||
export const createContainer = async (
|
|
||||||
databaseName: string,
|
|
||||||
containerName: string,
|
|
||||||
explorer: Explorer,
|
|
||||||
): Promise<ViewModels.Collection> => {
|
|
||||||
const createRequest: DataModels.CreateCollectionParams = {
|
|
||||||
createNewDatabase: false,
|
|
||||||
collectionId: containerName,
|
|
||||||
databaseId: databaseName,
|
|
||||||
databaseLevelThroughput: false,
|
|
||||||
};
|
|
||||||
await createCollection(createRequest);
|
|
||||||
await explorer.refreshAllDatabases();
|
|
||||||
const database = useDatabases.getState().findDatabaseWithId(databaseName);
|
|
||||||
if (!database) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
await database.loadCollections();
|
|
||||||
const newCollection = database.findCollectionWithId(containerName);
|
|
||||||
return newCollection;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const importData = async (collection: ViewModels.Collection): Promise<void> => {
|
|
||||||
// TODO: keep same chunk as ContainerSampleGenerator
|
|
||||||
const dataFileContent = await import(
|
|
||||||
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
|
|
||||||
);
|
|
||||||
await collection.bulkInsertDocuments(dataFileContent.data);
|
|
||||||
};
|
|
||||||
@@ -16,20 +16,10 @@ export const ConnectTab: React.FC = (): JSX.Element => {
|
|||||||
const [primaryReadonlyMasterKey, setPrimaryReadonlyMasterKey] = useState<string>("");
|
const [primaryReadonlyMasterKey, setPrimaryReadonlyMasterKey] = useState<string>("");
|
||||||
const [secondaryReadonlyMasterKey, setSecondaryReadonlyMasterKey] = useState<string>("");
|
const [secondaryReadonlyMasterKey, setSecondaryReadonlyMasterKey] = useState<string>("");
|
||||||
const uri: string = userContext.databaseAccount.properties?.documentEndpoint;
|
const uri: string = userContext.databaseAccount.properties?.documentEndpoint;
|
||||||
const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey};`;
|
const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey}`;
|
||||||
const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey};`;
|
const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey}`;
|
||||||
const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey};`;
|
const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey}`;
|
||||||
const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey};`;
|
const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey}`;
|
||||||
const maskedValue: string =
|
|
||||||
"*********************************************************************************************************************************";
|
|
||||||
const [showPrimaryMasterKey, setShowPrimaryMasterKey] = useState<boolean>(false);
|
|
||||||
const [showSecondaryMasterKey, setShowSecondaryMasterKey] = useState<boolean>(false);
|
|
||||||
const [showPrimaryReadonlyMasterKey, setShowPrimaryReadonlyMasterKey] = useState<boolean>(false);
|
|
||||||
const [showSecondaryReadonlyMasterKey, setShowSecondaryReadonlyMasterKey] = useState<boolean>(false);
|
|
||||||
const [showPrimaryConnectionStr, setShowPrimaryConnectionStr] = useState<boolean>(false);
|
|
||||||
const [showSecondaryConnectionStr, setShowSecondaryConnectionStr] = useState<boolean>(false);
|
|
||||||
const [showPrimaryReadonlyConnectionStr, setShowPrimaryReadonlyConnectionStr] = useState<boolean>(false);
|
|
||||||
const [showSecondaryReadonlyConnectionStr, setShowSecondaryReadonlyConnectionStr] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchKeys();
|
fetchKeys();
|
||||||
@@ -72,97 +62,55 @@ export const ConnectTab: React.FC = (): JSX.Element => {
|
|||||||
root: { width: "100%" },
|
root: { width: "100%" },
|
||||||
field: { backgroundColor: "rgb(230, 230, 230)" },
|
field: { backgroundColor: "rgb(230, 230, 230)" },
|
||||||
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
|
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
|
||||||
suffix: {
|
|
||||||
backgroundColor: "rgb(230, 230, 230)",
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCopyButton = (selector: string) => (
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Copy" }}
|
|
||||||
onClick={() => onCopyBtnClicked(selector)}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "rgb(230, 230, 230)",
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
rootHovered: {
|
|
||||||
backgroundColor: "rgb(220, 220, 220)",
|
|
||||||
},
|
|
||||||
rootPressed: {
|
|
||||||
backgroundColor: "rgb(210, 210, 210)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", padding: 16 }}>
|
<div style={{ width: "100%", padding: 16 }}>
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 16, margin: 10 }}>
|
|
||||||
<TextField
|
|
||||||
label="URI"
|
|
||||||
id="uriTextfield"
|
|
||||||
readOnly
|
|
||||||
value={uri}
|
|
||||||
styles={textfieldStyles}
|
|
||||||
onRenderSuffix={() => renderCopyButton("#uriTextfield")}
|
|
||||||
/>
|
|
||||||
<div style={{ width: 32 }}></div>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Pivot>
|
<Pivot>
|
||||||
{userContext.hasWriteAccess && (
|
{userContext.hasWriteAccess && (
|
||||||
<PivotItem headerText="Read-write Keys">
|
<PivotItem headerText="Read-write Keys">
|
||||||
<Stack style={{ margin: 10, overflow: "auto", maxHeight: "calc(100vh - 300px)" }}>
|
<Stack style={{ margin: 10 }}>
|
||||||
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
|
<TextField label="URI" id="uriTextfield" readOnly value={uri} styles={textfieldStyles} />
|
||||||
|
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriTextfield")} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="PRIMARY KEY"
|
label="PRIMARY KEY"
|
||||||
id="primaryKeyTextfield"
|
id="primaryKeyTextfield"
|
||||||
readOnly
|
readOnly
|
||||||
value={showPrimaryMasterKey ? primaryMasterKey : maskedValue}
|
value={primaryMasterKey}
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showPrimaryMasterKey && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#primaryKeyTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: showPrimaryMasterKey ? "Hide3" : "View" }}
|
|
||||||
onClick={() => setShowPrimaryMasterKey(!showPrimaryMasterKey)}
|
|
||||||
/>
|
/>
|
||||||
|
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#primaryKeyTextfield")} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="SECONDARY KEY"
|
label="SECONDARY KEY"
|
||||||
id="secondaryKeyTextfield"
|
id="secondaryKeyTextfield"
|
||||||
readOnly
|
readOnly
|
||||||
value={showSecondaryMasterKey ? secondaryMasterKey : maskedValue}
|
value={secondaryMasterKey}
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showSecondaryMasterKey && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#secondaryKeyTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: showSecondaryMasterKey ? "Hide3" : "View" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
onClick={() => setShowSecondaryMasterKey(!showSecondaryMasterKey)}
|
onClick={() => onCopyBtnClicked("#secondaryKeyTextfield")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="PRIMARY CONNECTION STRING"
|
label="PRIMARY CONNECTION STRING"
|
||||||
id="primaryConStrTextfield"
|
id="primaryConStrTextfield"
|
||||||
readOnly
|
readOnly
|
||||||
value={showPrimaryConnectionStr ? primaryConnectionStr : maskedValue}
|
value={primaryConnectionStr}
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showPrimaryConnectionStr && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#primaryConStrTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: showPrimaryConnectionStr ? "Hide3" : "View" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
onClick={() => setShowPrimaryConnectionStr(!showPrimaryConnectionStr)}
|
onClick={() => onCopyBtnClicked("#primaryConStrTextfield")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
@@ -170,36 +118,34 @@ export const ConnectTab: React.FC = (): JSX.Element => {
|
|||||||
label="SECONDARY CONNECTION STRING"
|
label="SECONDARY CONNECTION STRING"
|
||||||
id="secondaryConStrTextfield"
|
id="secondaryConStrTextfield"
|
||||||
readOnly
|
readOnly
|
||||||
value={showSecondaryConnectionStr ? secondaryConnectionStr : maskedValue}
|
value={secondaryConnectionStr}
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showSecondaryConnectionStr && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#secondaryConStrTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: showSecondaryConnectionStr ? "Hide3" : "View" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
onClick={() => setShowSecondaryConnectionStr(!showSecondaryConnectionStr)}
|
onClick={() => onCopyBtnClicked("#secondaryConStrTextfield")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
)}
|
)}
|
||||||
<PivotItem headerText="Read-only Keys">
|
<PivotItem headerText="Read-only Keys">
|
||||||
<Stack style={{ margin: 10, overflow: "auto", maxHeight: "calc(100vh - 300px)" }}>
|
<Stack style={{ margin: 10 }}>
|
||||||
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
|
<TextField label="URI" id="uriReadOnlyTextfield" readOnly value={uri} styles={textfieldStyles} />
|
||||||
|
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriReadOnlyTextfield")} />
|
||||||
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="PRIMARY READ-ONLY KEY"
|
label="PRIMARY READ-ONLY KEY"
|
||||||
id="primaryReadonlyKeyTextfield"
|
id="primaryReadonlyKeyTextfield"
|
||||||
readOnly
|
readOnly
|
||||||
value={showPrimaryReadonlyMasterKey ? primaryReadonlyMasterKey : maskedValue}
|
value={primaryReadonlyMasterKey}
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showPrimaryReadonlyMasterKey && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#primaryReadonlyKeyTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: showPrimaryReadonlyMasterKey ? "Hide3" : "View" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
onClick={() => setShowPrimaryReadonlyMasterKey(!showPrimaryReadonlyMasterKey)}
|
onClick={() => onCopyBtnClicked("#primaryReadonlyKeyTextfield")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
@@ -207,15 +153,12 @@ export const ConnectTab: React.FC = (): JSX.Element => {
|
|||||||
label="SECONDARY READ-ONLY KEY"
|
label="SECONDARY READ-ONLY KEY"
|
||||||
id="secondaryReadonlyKeyTextfield"
|
id="secondaryReadonlyKeyTextfield"
|
||||||
readOnly
|
readOnly
|
||||||
value={showSecondaryReadonlyMasterKey ? secondaryReadonlyMasterKey : maskedValue}
|
value={secondaryReadonlyMasterKey}
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showSecondaryReadonlyMasterKey && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#secondaryReadonlyKeyTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: showSecondaryReadonlyMasterKey ? "Hide3" : "View" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
onClick={() => setShowSecondaryReadonlyMasterKey(!showSecondaryReadonlyMasterKey)}
|
onClick={() => onCopyBtnClicked("#secondaryReadonlyKeyTextfield")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
@@ -223,31 +166,25 @@ export const ConnectTab: React.FC = (): JSX.Element => {
|
|||||||
label="PRIMARY READ-ONLY CONNECTION STRING"
|
label="PRIMARY READ-ONLY CONNECTION STRING"
|
||||||
id="primaryReadonlyConStrTextfield"
|
id="primaryReadonlyConStrTextfield"
|
||||||
readOnly
|
readOnly
|
||||||
value={showPrimaryReadonlyConnectionStr ? primaryReadonlyConnectionStr : maskedValue}
|
value={primaryReadonlyConnectionStr}
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showPrimaryReadonlyConnectionStr && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#primaryReadonlyConStrTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: showPrimaryReadonlyConnectionStr ? "Hide3" : "View" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
onClick={() => setShowPrimaryReadonlyConnectionStr(!showPrimaryReadonlyConnectionStr)}
|
onClick={() => onCopyBtnClicked("#primaryReadonlyConStrTextfield")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="SECONDARY READ-ONLY CONNECTION STRING"
|
label="SECONDARY READ-ONLY CONNECTION STRING"
|
||||||
id="secondaryReadonlyConStrTextfield"
|
id="secondaryReadonlyConStrTextfield"
|
||||||
value={showSecondaryReadonlyConnectionStr ? secondaryReadonlyConnectionStr : maskedValue}
|
value={secondaryReadonlyConnectionStr}
|
||||||
readOnly
|
readOnly
|
||||||
styles={textfieldStyles}
|
styles={textfieldStyles}
|
||||||
{...(showSecondaryReadonlyConnectionStr && {
|
|
||||||
onRenderSuffix: () => renderCopyButton("#secondaryReadonlyConStrTextfield"),
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconProps={{ iconName: showSecondaryReadonlyConnectionStr ? "Hide3" : "View" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
onClick={() => setShowSecondaryReadonlyConnectionStr(!showSecondaryReadonlyConnectionStr)}
|
onClick={() => onCopyBtnClicked("#secondaryReadonlyConStrTextfield")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -49,14 +49,12 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
|
||||||
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
||||||
import NewDocumentIcon from "../../../../images/NewDocument.svg";
|
import NewDocumentIcon from "../../../../images/NewDocument.svg";
|
||||||
import UploadIcon from "../../../../images/Upload_16x16.svg";
|
import UploadIcon from "../../../../images/Upload_16x16.svg";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||||
@@ -133,14 +131,6 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
refreshBtn: {
|
|
||||||
position: "absolute",
|
|
||||||
top: "3px",
|
|
||||||
right: "4px",
|
|
||||||
float: "right",
|
|
||||||
zIndex: 1,
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
deleteProgressContent: {
|
deleteProgressContent: {
|
||||||
paddingTop: tokens.spacingVerticalL,
|
paddingTop: tokens.spacingVerticalL,
|
||||||
},
|
},
|
||||||
@@ -306,7 +296,6 @@ export type ButtonsDependencies = {
|
|||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
editorState: ViewModels.DocumentExplorerState;
|
editorState: ViewModels.DocumentExplorerState;
|
||||||
isPreferredApiMongoDB: boolean;
|
isPreferredApiMongoDB: boolean;
|
||||||
clientWriteEnabled: boolean;
|
|
||||||
onNewDocumentClick: UiKeyboardEvent;
|
onNewDocumentClick: UiKeyboardEvent;
|
||||||
onSaveNewDocumentClick: UiKeyboardEvent;
|
onSaveNewDocumentClick: UiKeyboardEvent;
|
||||||
onRevertNewDocumentClick: UiKeyboardEvent;
|
onRevertNewDocumentClick: UiKeyboardEvent;
|
||||||
@@ -330,7 +319,6 @@ const createUploadButton = (container: Explorer): CommandButtonComponentProps =>
|
|||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled:
|
disabled:
|
||||||
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
|
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
|
||||||
!useClientWriteEnabled.getState().clientWriteEnabled ||
|
|
||||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -349,7 +337,6 @@ export const getTabsButtons = ({
|
|||||||
selectedRows,
|
selectedRows,
|
||||||
editorState,
|
editorState,
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
clientWriteEnabled,
|
|
||||||
onNewDocumentClick,
|
onNewDocumentClick,
|
||||||
onSaveNewDocumentClick,
|
onSaveNewDocumentClick,
|
||||||
onRevertNewDocumentClick,
|
onRevertNewDocumentClick,
|
||||||
@@ -375,7 +362,6 @@ export const getTabsButtons = ({
|
|||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
!getNewDocumentButtonState(editorState).enabled ||
|
!getNewDocumentButtonState(editorState).enabled ||
|
||||||
!clientWriteEnabled ||
|
|
||||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
id: NEW_DOCUMENT_BUTTON_ID,
|
id: NEW_DOCUMENT_BUTTON_ID,
|
||||||
});
|
});
|
||||||
@@ -393,7 +379,6 @@ export const getTabsButtons = ({
|
|||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
!getSaveNewDocumentButtonState(editorState).enabled ||
|
!getSaveNewDocumentButtonState(editorState).enabled ||
|
||||||
!clientWriteEnabled ||
|
|
||||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
id: SAVE_BUTTON_ID,
|
id: SAVE_BUTTON_ID,
|
||||||
});
|
});
|
||||||
@@ -428,7 +413,6 @@ export const getTabsButtons = ({
|
|||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
!getSaveExistingDocumentButtonState(editorState).enabled ||
|
!getSaveExistingDocumentButtonState(editorState).enabled ||
|
||||||
!clientWriteEnabled ||
|
|
||||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
id: UPDATE_BUTTON_ID,
|
id: UPDATE_BUTTON_ID,
|
||||||
});
|
});
|
||||||
@@ -461,7 +445,7 @@ export const getTabsButtons = ({
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected() || !clientWriteEnabled,
|
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
id: DELETE_BUTTON_ID,
|
id: DELETE_BUTTON_ID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -635,7 +619,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
);
|
);
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const clientWriteEnabled = useClientWriteEnabled((state) => state.clientWriteEnabled);
|
|
||||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||||
leftPaneWidthPercent: 35,
|
leftPaneWidthPercent: 35,
|
||||||
@@ -773,14 +756,16 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
[_collection, _partitionKey],
|
[_collection, _partitionKey],
|
||||||
);
|
);
|
||||||
const partitionKeyPropertyHeaders: string[] = useMemo(
|
const partitionKeyPropertyHeaders: string[] = useMemo(
|
||||||
() => (partitionKey?.systemKey ? [] : _collection?.partitionKeyPropertyHeaders || partitionKey?.paths),
|
() => _collection?.partitionKeyPropertyHeaders || partitionKey?.paths,
|
||||||
[_collection?.partitionKeyPropertyHeaders, partitionKey?.paths, partitionKey?.systemKey],
|
[_collection?.partitionKeyPropertyHeaders, partitionKey?.paths],
|
||||||
|
);
|
||||||
|
let partitionKeyProperties = useMemo(
|
||||||
|
() =>
|
||||||
|
partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
|
||||||
|
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
|
||||||
|
),
|
||||||
|
[partitionKeyPropertyHeaders],
|
||||||
);
|
);
|
||||||
let partitionKeyProperties = useMemo(() => {
|
|
||||||
return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
|
|
||||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
|
|
||||||
);
|
|
||||||
}, [partitionKeyPropertyHeaders]);
|
|
||||||
|
|
||||||
const getInitialColumnSelection = () => {
|
const getInitialColumnSelection = () => {
|
||||||
const defaultColumnsIds = ["id"];
|
const defaultColumnsIds = ["id"];
|
||||||
@@ -871,7 +856,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedRows,
|
selectedRows,
|
||||||
editorState,
|
editorState,
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
clientWriteEnabled,
|
|
||||||
onNewDocumentClick,
|
onNewDocumentClick,
|
||||||
onSaveNewDocumentClick,
|
onSaveNewDocumentClick,
|
||||||
onRevertNewDocumentClick,
|
onRevertNewDocumentClick,
|
||||||
@@ -1044,7 +1028,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedDocumentId = documentIds[clickedRowIndex as number];
|
const selectedDocumentId = documentIds[clickedRowIndex as number];
|
||||||
const originalPartitionKeyValue = selectedDocumentId.partitionKeyValue;
|
|
||||||
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
||||||
|
|
||||||
onExecutionErrorChange(false);
|
onExecutionErrorChange(false);
|
||||||
@@ -1080,10 +1063,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setColumnDefinitionsFromDocument(documentContent);
|
setColumnDefinitionsFromDocument(documentContent);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// in case of any kind of failures of accidently changing partition key, restore the original
|
|
||||||
// so that when user navigates away from current document and comes back,
|
|
||||||
// it doesnt fail to load due to using the invalid partition keys
|
|
||||||
selectedDocumentId.partitionKeyValue = originalPartitionKeyValue;
|
|
||||||
onExecutionErrorChange(true);
|
onExecutionErrorChange(true);
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
|
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
|
||||||
@@ -1291,7 +1270,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedRows,
|
selectedRows,
|
||||||
editorState,
|
editorState,
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
clientWriteEnabled,
|
|
||||||
onNewDocumentClick,
|
onNewDocumentClick,
|
||||||
onSaveNewDocumentClick,
|
onSaveNewDocumentClick,
|
||||||
onRevertNewDocumentClick,
|
onRevertNewDocumentClick,
|
||||||
@@ -1304,7 +1282,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedRows,
|
selectedRows,
|
||||||
editorState,
|
editorState,
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
clientWriteEnabled,
|
|
||||||
onNewDocumentClick,
|
onNewDocumentClick,
|
||||||
onSaveNewDocumentClick,
|
onSaveNewDocumentClick,
|
||||||
onRevertNewDocumentClick,
|
onRevertNewDocumentClick,
|
||||||
@@ -1723,8 +1700,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
renderObjectForEditor = (value: unknown): string => MongoUtility.tojson(value, null, false);
|
renderObjectForEditor = (value: unknown): string => MongoUtility.tojson(value, null, false);
|
||||||
|
|
||||||
const _hasShardKeySpecified = (document: unknown): boolean => {
|
const _hasShardKeySpecified = (document: unknown): boolean => {
|
||||||
const partitionKeyDefinition: PartitionKeyDefinition = _getPartitionKeyDefinition() as PartitionKeyDefinition;
|
return Boolean(extractPartitionKeyValues(document, _getPartitionKeyDefinition() as PartitionKeyDefinition));
|
||||||
return partitionKeyDefinition.systemKey || Boolean(extractPartitionKeyValues(document, partitionKeyDefinition));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getPartitionKeyDefinition = (): DataModels.PartitionKey => {
|
const _getPartitionKeyDefinition = (): DataModels.PartitionKey => {
|
||||||
@@ -1748,7 +1724,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return partitionKey;
|
return partitionKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
partitionKeyProperties = partitionKeyProperties.map((partitionKeyProperty, i) => {
|
partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => {
|
||||||
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
|
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
|
||||||
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
|
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
|
||||||
}
|
}
|
||||||
@@ -2098,8 +2074,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CosmosFluentProvider className={styles.container}>
|
<CosmosFluentProvider className={styles.container}>
|
||||||
<div data-test={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||||
<div data-test={"DocumentsTab/Filter"} className={styles.filterRow}>
|
<div className={styles.filterRow}>
|
||||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||||
<InputDataList
|
<InputDataList
|
||||||
dropdownOptions={getFilterChoices()}
|
dropdownOptions={getFilterChoices()}
|
||||||
@@ -2141,11 +2117,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
||||||
<div
|
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
||||||
data-test={"DocumentsTab/DocumentsPane"}
|
|
||||||
style={{ height: "100%", width: "100%", overflow: "hidden" }}
|
|
||||||
ref={tableContainerRef}
|
|
||||||
>
|
|
||||||
<div className={styles.tableContainer}>
|
<div className={styles.tableContainer}>
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
@@ -2172,18 +2144,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{tableItems.length > 0 && (
|
{tableItems.length > 0 && (
|
||||||
<a
|
<a
|
||||||
@@ -2199,7 +2159,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
</div>
|
</div>
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
<Allotment.Pane minSize={30}>
|
<Allotment.Pane minSize={30}>
|
||||||
<div data-test={"DocumentsTab/ResultsPane"} style={{ height: "100%", width: "100%" }}>
|
<div style={{ height: "100%", width: "100%" }}>
|
||||||
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
||||||
<EditorReact
|
<EditorReact
|
||||||
language={"json"}
|
language={"json"}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
aria-label="Select column"
|
aria-label="Select column"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<MoreHorizontalRegular />}
|
icon={<MoreHorizontalRegular />}
|
||||||
style={{ position: "absolute", right: 10, backgroundColor: tokens.colorNeutralBackground1 }}
|
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||||
/>
|
/>
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuPopover>
|
<MenuPopover>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="tab-pane active"
|
className="tab-pane active"
|
||||||
data-test="DocumentsTab"
|
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@@ -16,7 +15,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
||||||
data-test="DocumentsTab/Filter"
|
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
SELECT * FROM c
|
SELECT * FROM c
|
||||||
@@ -67,7 +65,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
preferredSize="35%"
|
preferredSize="35%"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-test="DocumentsTab/DocumentsPane"
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"height": "100%",
|
"height": "100%",
|
||||||
@@ -129,7 +126,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
minSize={30}
|
minSize={30}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-test="DocumentsTab/ResultsPane"
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"height": "100%",
|
"height": "100%",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
|
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
|
||||||
import { AuthType } from "AuthType";
|
|
||||||
import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError";
|
import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError";
|
||||||
import { SplitterDirection } from "Common/Splitter";
|
import { SplitterDirection } from "Common/Splitter";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
@@ -22,7 +21,6 @@ import { QueryConstants } from "Shared/Constants";
|
|||||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { TabsState, useTabs } from "hooks/useTabs";
|
import { TabsState, useTabs } from "hooks/useTabs";
|
||||||
import React, { Fragment, createRef } from "react";
|
import React, { Fragment, createRef } from "react";
|
||||||
@@ -486,9 +484,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled: !this.saveQueryButton.enabled,
|
||||||
!this.saveQueryButton.enabled ||
|
|
||||||
(!useClientWriteEnabled.getState().clientWriteEnabled && userContext.authType === AuthType.AAD),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +696,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private unsubscribeCopilotSidebar: () => void;
|
private unsubscribeCopilotSidebar: () => void;
|
||||||
private unsubscribeClientWriteEnabled: () => void;
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
useTabs.subscribe((state: TabsState) => {
|
useTabs.subscribe((state: TabsState) => {
|
||||||
@@ -717,17 +712,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
|
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
document.addEventListener("keydown", this.handleCopilotKeyDown);
|
document.addEventListener("keydown", this.handleCopilotKeyDown);
|
||||||
|
|
||||||
this.unsubscribeClientWriteEnabled = useClientWriteEnabled.subscribe(() => {
|
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
document.removeEventListener("keydown", this.handleCopilotKeyDown);
|
document.removeEventListener("keydown", this.handleCopilotKeyDown);
|
||||||
if (this.unsubscribeClientWriteEnabled) {
|
|
||||||
this.unsubscribeClientWriteEnabled();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEditorAndQueryResult(): JSX.Element {
|
private getEditorAndQueryResult(): JSX.Element {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
|
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
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 = editable.observable<string>();
|
||||||
this.id.validations([IsValidCosmosDbResourceId]);
|
this.id.validations([ScriptTabBase._isValidId]);
|
||||||
|
|
||||||
this.editorContent = editable.observable<string>();
|
this.editorContent = editable.observable<string>();
|
||||||
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
|
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
|
||||||
@@ -263,6 +262,29 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
|||||||
this.updateNavbarWithTabsButtons();
|
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 {
|
private static _isNotEmpty(value: string): boolean {
|
||||||
return !!value;
|
return !!value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||||
import { Pivot, PivotItem } from "@fluentui/react";
|
import { Pivot, PivotItem } from "@fluentui/react";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
import DiscardIcon from "../../../../images/discard.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 {
|
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
const isValidId: boolean = event.currentTarget.reportValidity();
|
|
||||||
if (this.state.saveButton.visible) {
|
if (this.state.saveButton.visible) {
|
||||||
this.setState({
|
this.setState({
|
||||||
id: event.target.value,
|
id: event.target.value,
|
||||||
saveButton: {
|
saveButton: {
|
||||||
enabled: isValidId,
|
enabled: true,
|
||||||
visible: this.props.scriptTabBaseInstance.isNew(),
|
visible: this.props.scriptTabBaseInstance.isNew(),
|
||||||
},
|
},
|
||||||
discardButton: {
|
discardButton: {
|
||||||
@@ -530,8 +528,8 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
className="formTree"
|
className="formTree"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
aria-label="Stored procedure id"
|
aria-label="Stored procedure id"
|
||||||
placeholder="Enter the new stored procedure id"
|
placeholder="Enter the new stored procedure id"
|
||||||
size={40}
|
size={40}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||||
import VcoreFirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
|
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -43,11 +42,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
return (
|
return (
|
||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
screenshot={
|
screenshot={FirewallRuleScreenshot}
|
||||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
|
||||||
? VcoreFirewallRuleScreenshot
|
|
||||||
: FirewallRuleScreenshot
|
|
||||||
}
|
|
||||||
shellName={this.getShellNameForDisplay(this.kind)}
|
shellName={this.getShellNameForDisplay(this.kind)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TriggerDefinition } from "@azure/cosmos";
|
import { TriggerDefinition } from "@azure/cosmos";
|
||||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.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 {
|
private isNotEmpty(value: string): boolean {
|
||||||
return !!value;
|
return !!value;
|
||||||
}
|
}
|
||||||
@@ -264,13 +286,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
newValue?: string,
|
newValue?: string,
|
||||||
): void => {
|
): void => {
|
||||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||||
let isValidId: boolean = true;
|
|
||||||
if (inputElement) {
|
|
||||||
isValidId = inputElement.reportValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
|
||||||
this.setState({ triggerId: newValue });
|
this.setState({ triggerId: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -297,8 +313,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
|||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
|
||||||
placeholder="Enter the new trigger id"
|
placeholder="Enter the new trigger id"
|
||||||
size={40}
|
size={40}
|
||||||
value={triggerId}
|
value={triggerId}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { Label, TextField } from "@fluentui/react";
|
import { Label, TextField } from "@fluentui/react";
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
@@ -65,13 +64,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
newValue?: string,
|
newValue?: string,
|
||||||
): void => {
|
): void => {
|
||||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||||
let isValidId: boolean = true;
|
|
||||||
if (inputElement) {
|
|
||||||
isValidId = inputElement.reportValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
|
||||||
this.setState({ udfId: 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 {
|
private isNotEmpty(value: string): boolean {
|
||||||
return !!value;
|
return !!value;
|
||||||
}
|
}
|
||||||
@@ -268,8 +284,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
|||||||
required
|
required
|
||||||
readOnly={!isUdfIdEditable}
|
readOnly={!isUdfIdEditable}
|
||||||
type="text"
|
type="text"
|
||||||
pattern={ValidCosmosDbIdInputPattern.source}
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title={ValidCosmosDbIdDescription}
|
|
||||||
placeholder="Enter the new user defined function id"
|
placeholder="Enter the new user defined function id"
|
||||||
size={40}
|
size={40}
|
||||||
value={udfId}
|
value={udfId}
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
JSONObject,
|
|
||||||
Resource,
|
|
||||||
StoredProcedureDefinition,
|
|
||||||
TriggerDefinition,
|
|
||||||
UserDefinedFunctionDefinition,
|
|
||||||
} from "@azure/cosmos";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||||
@@ -1050,22 +1044,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
||||||
try {
|
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
||||||
TelemetryProcessor.trace(Action.UploadDocuments, ActionModifiers.Start, {
|
|
||||||
nbFiles: files.length,
|
return { data };
|
||||||
});
|
|
||||||
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
|
||||||
TelemetryProcessor.trace(Action.UploadDocuments, ActionModifiers.Success, {
|
|
||||||
nbFiles: files.length,
|
|
||||||
});
|
|
||||||
return { data };
|
|
||||||
} catch (error) {
|
|
||||||
TelemetryProcessor.trace(Action.UploadDocuments, ActionModifiers.Failed, {
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private uploadFile(file: File): Promise<UploadDetailsRecord> {
|
private uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||||
@@ -1092,56 +1073,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bulkInsertDocuments(documents: JSONObject[]): Promise<{
|
|
||||||
numSucceeded: number;
|
|
||||||
numFailed: number;
|
|
||||||
numThrottled: number;
|
|
||||||
errors: string[];
|
|
||||||
}> {
|
|
||||||
const stats = {
|
|
||||||
numSucceeded: 0,
|
|
||||||
numFailed: 0,
|
|
||||||
numThrottled: 0,
|
|
||||||
errors: [] as string[],
|
|
||||||
};
|
|
||||||
|
|
||||||
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
|
|
||||||
const chunkedContent = Array.from({ length: Math.ceil(documents.length / chunkSize) }, (_, index) =>
|
|
||||||
documents.slice(index * chunkSize, index * chunkSize + chunkSize),
|
|
||||||
);
|
|
||||||
for (const chunk of chunkedContent) {
|
|
||||||
let retryAttempts = 0;
|
|
||||||
let chunkComplete = false;
|
|
||||||
let documentsToAttempt = chunk;
|
|
||||||
while (retryAttempts < 10 && !chunkComplete) {
|
|
||||||
const responses = await bulkCreateDocument(this, documentsToAttempt);
|
|
||||||
const attemptedDocuments = [...documentsToAttempt];
|
|
||||||
documentsToAttempt = [];
|
|
||||||
responses.forEach((response, index) => {
|
|
||||||
if (response.statusCode === 201) {
|
|
||||||
stats.numSucceeded++;
|
|
||||||
} else if (response.statusCode === 429) {
|
|
||||||
documentsToAttempt.push(attemptedDocuments[index]);
|
|
||||||
} else {
|
|
||||||
stats.numFailed++;
|
|
||||||
stats.errors.push(JSON.stringify(response.resourceBody));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (documentsToAttempt.length === 0) {
|
|
||||||
chunkComplete = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
logConsoleInfo(
|
|
||||||
`${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`,
|
|
||||||
);
|
|
||||||
retryAttempts++;
|
|
||||||
await sleep(retryAttempts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
||||||
const record: UploadDetailsRecord = {
|
const record: UploadDetailsRecord = {
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
@@ -1154,11 +1085,38 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
try {
|
try {
|
||||||
const parsedContent = JSON.parse(documentContent);
|
const parsedContent = JSON.parse(documentContent);
|
||||||
if (Array.isArray(parsedContent)) {
|
if (Array.isArray(parsedContent)) {
|
||||||
const { numSucceeded, numFailed, numThrottled, errors } = await this.bulkInsertDocuments(parsedContent);
|
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
|
||||||
record.numSucceeded = numSucceeded;
|
const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) =>
|
||||||
record.numFailed = numFailed;
|
parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize),
|
||||||
record.numThrottled = numThrottled;
|
);
|
||||||
record.errors = errors;
|
for (const chunk of chunkedContent) {
|
||||||
|
let retryAttempts = 0;
|
||||||
|
let chunkComplete = false;
|
||||||
|
let documentsToAttempt = chunk;
|
||||||
|
while (retryAttempts < 10 && !chunkComplete) {
|
||||||
|
const responses = await bulkCreateDocument(this, documentsToAttempt);
|
||||||
|
const attemptedDocuments = [...documentsToAttempt];
|
||||||
|
documentsToAttempt = [];
|
||||||
|
responses.forEach((response, index) => {
|
||||||
|
if (response.statusCode === 201) {
|
||||||
|
record.numSucceeded++;
|
||||||
|
} else if (response.statusCode === 429) {
|
||||||
|
documentsToAttempt.push(attemptedDocuments[index]);
|
||||||
|
} else {
|
||||||
|
record.numFailed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (documentsToAttempt.length === 0) {
|
||||||
|
chunkComplete = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
logConsoleInfo(
|
||||||
|
`${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`,
|
||||||
|
);
|
||||||
|
retryAttempts++;
|
||||||
|
await sleep(retryAttempts);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await createDocument(this, parsedContent);
|
await createDocument(this, parsedContent);
|
||||||
record.numSucceeded++;
|
record.numSucceeded++;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
||||||
import { Home16Regular } from "@fluentui/react-icons";
|
import { Home16Regular } from "@fluentui/react-icons";
|
||||||
import { AuthType } from "AuthType";
|
import { AuthType } from "AuthType";
|
||||||
|
import { Collection } from "Contracts/ViewModels";
|
||||||
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
||||||
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import {
|
import {
|
||||||
@@ -60,7 +61,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
|||||||
|
|
||||||
const databaseTreeNodes = useMemo(() => {
|
const databaseTreeNodes = useMemo(() => {
|
||||||
return userContext.authType === AuthType.ResourceToken
|
return userContext.authType === AuthType.ResourceToken
|
||||||
? createResourceTokenTreeNodes(resourceTokenCollection)
|
? createResourceTokenTreeNodes(resourceTokenCollection as Collection)
|
||||||
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
||||||
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
||||||
|
|
||||||
|
|||||||
@@ -740,38 +740,12 @@ 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 (native) 1`] = `
|
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"children": [
|
"children": undefined,
|
||||||
{
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"id": "",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Items",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Settings",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
{
|
{
|
||||||
@@ -798,38 +772,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onExpanded": [Function],
|
"onExpanded": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"children": [
|
"children": undefined,
|
||||||
{
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"id": "",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Items",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Settings",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Conflicts",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
{
|
{
|
||||||
@@ -863,6 +806,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"label": "New Container",
|
"label": "New Container",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Delete Database",
|
||||||
|
"onClick": [Function],
|
||||||
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
"iconSrc": <DatabaseRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
@@ -877,33 +826,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"children": [
|
"children": undefined,
|
||||||
{
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"id": "sampleItems",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Items",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sampleSettings",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Settings",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
{
|
{
|
||||||
@@ -937,6 +860,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"label": "New Container",
|
"label": "New Container",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Delete Database",
|
||||||
|
"onClick": [Function],
|
||||||
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
"iconSrc": <DatabaseRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
@@ -951,88 +880,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"children": [
|
"children": undefined,
|
||||||
{
|
|
||||||
"contextMenu": [
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "New SQL Query",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"iconSrc": {},
|
|
||||||
"label": "Delete Container",
|
|
||||||
"onClick": [Function],
|
|
||||||
"styleClass": "deleteCollectionMenuItem",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"id": "",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Items",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"isSelected": [Function],
|
|
||||||
"label": "Settings",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: false",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "street",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: true",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "line2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "number",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: false",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "zip",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "HasNulls: false",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "orderId",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"label": "Schema",
|
|
||||||
"onClick": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
{
|
{
|
||||||
@@ -1071,6 +919,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"label": "New Container",
|
"label": "New Container",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Delete Database",
|
||||||
|
"onClick": [Function],
|
||||||
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
"iconSrc": <DatabaseRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
@@ -1085,7 +939,7 @@ 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 (mirrored) 1`] = `
|
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only 1`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
@@ -2412,7 +2266,7 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"iconSrc": <DocumentMultipleRegular
|
"iconSrc": <EyeRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ jest.mock("Explorer/Tree/Trigger", () => {
|
|||||||
jest.mock("Common/DatabaseAccountUtility", () => {
|
jest.mock("Common/DatabaseAccountUtility", () => {
|
||||||
return {
|
return {
|
||||||
isPublicInternetAccessAllowed: () => true,
|
isPublicInternetAccessAllowed: () => true,
|
||||||
isGlobalSecondaryIndexEnabled: () => false,
|
isMaterializedViewsEnabled: () => false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -373,28 +373,18 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
|
|
||||||
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
|
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
|
||||||
[
|
[
|
||||||
"the SQL API, on Fabric read-only (mirrored)",
|
"the SQL API, on Fabric read-only",
|
||||||
Platform.Fabric,
|
Platform.Fabric,
|
||||||
false,
|
false,
|
||||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||||
{
|
{ fabricContext: { isReadOnly: true } as FabricContext<CosmosDbArtifactType> },
|
||||||
fabricContext: {
|
|
||||||
isReadOnly: true,
|
|
||||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
|
||||||
} as FabricContext<CosmosDbArtifactType>,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"the SQL API, on Fabric non read-only (native)",
|
"the SQL API, on Fabric non read-only",
|
||||||
Platform.Fabric,
|
Platform.Fabric,
|
||||||
false,
|
false,
|
||||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||||
{
|
{ fabricContext: { isReadOnly: false } as FabricContext<CosmosDbArtifactType> },
|
||||||
fabricContext: {
|
|
||||||
isReadOnly: false,
|
|
||||||
artifactType: CosmosDbArtifactType.NATIVE,
|
|
||||||
} as FabricContext<CosmosDbArtifactType>,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"the SQL API, on Portal",
|
"the SQL API, on Portal",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
|||||||
import Trigger from "Explorer/Tree/Trigger";
|
import Trigger from "Explorer/Tree/Trigger";
|
||||||
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
|
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
@@ -23,13 +23,13 @@ import { useNotebook } from "../Notebook/useNotebook";
|
|||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
export const shouldShowScriptNodes = (): boolean => {
|
export const shouldShowScriptNodes = (): boolean => {
|
||||||
return !isFabric() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||||
};
|
};
|
||||||
|
|
||||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||||
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
||||||
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
||||||
const GlobalSecondaryIndexCollectionIcon = <EyeRegular fontSize={16} />; //check icon
|
const MaterializedViewCollectionIcon = <EyeRegular fontSize={16} />; //check icon
|
||||||
|
|
||||||
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
||||||
const updatedSampleTree: TreeNode = {
|
const updatedSampleTree: TreeNode = {
|
||||||
@@ -81,7 +81,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
|
|||||||
return [updatedSampleTree];
|
return [updatedSampleTree];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createResourceTokenTreeNodes = (collection: ViewModels.Collection): TreeNode[] => {
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -111,7 +111,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children,
|
children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
iconSrc: TreeCollectionIcon,
|
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
@@ -220,7 +220,7 @@ export const buildCollectionNode = (
|
|||||||
): TreeNode => {
|
): TreeNode => {
|
||||||
let children: TreeNode[];
|
let children: TreeNode[];
|
||||||
// Flat Tree for Fabric
|
// Flat Tree for Fabric
|
||||||
if (!isFabricMirrored()) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab);
|
children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ export const buildCollectionNode = (
|
|||||||
children: children,
|
children: children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||||
iconSrc: collection.materializedViewDefinition() ? GlobalSecondaryIndexCollectionIcon : TreeCollectionIcon,
|
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
@@ -318,7 +318,7 @@ const buildCollectionNodeChildren = (
|
|||||||
|
|
||||||
children.push({
|
children.push({
|
||||||
id,
|
id,
|
||||||
label: database.isDatabaseShared() || isServerlessAccount() || isFabricNative() ? "Settings" : "Scale & Settings",
|
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
|
||||||
onClick: collection.onSettingsClick.bind(collection),
|
onClick: collection.onSettingsClick.bind(collection),
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"MaterializedViewsBuilderDescription": "Provision a materialized views builder 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 materialized view 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": "Materialized views builder",
|
"MaterializedViewsBuilder": "Materializedviews Builder",
|
||||||
"Provisioned": "Provisioned",
|
"Provisioned": "Provisioned",
|
||||||
"Deprovisioned": "Deprovisioned",
|
"Deprovisioned": "Deprovisioned",
|
||||||
"LearnAboutMaterializedViews": "Learn more about materialized views.",
|
"LearnAboutMaterializedViews": "Learn more about materializedviews.",
|
||||||
"DeprovisioningDetailsText": "Learn more about materialized views.",
|
"DeprovisioningDetailsText": "Learn more about materializedviews.",
|
||||||
"MaterializedviewsBuilderPricing": "Learn more about materialized views pricing.",
|
"MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.",
|
||||||
"SKUs": "SKUs",
|
"SKUs": "SKUs",
|
||||||
"SKUsPlaceHolder": "Select SKUs",
|
"SKUsPlaceHolder": "Select SKUs",
|
||||||
"NumberOfInstances": "Number of instances",
|
"NumberOfInstances": "Number of instances",
|
||||||
@@ -14,58 +14,35 @@
|
|||||||
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 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)",
|
"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)",
|
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
|
||||||
"CreateMessage": "Materialized views builder resource is being created.",
|
"CreateMessage": "MaterializedViewsBuilder resource is being created.",
|
||||||
"CreateInitializeTitle": "Provisioning resource",
|
"CreateInitializeTitle": "Provisioning resource",
|
||||||
"CreateInitializeMessage": "Materialized views builder resource will be provisioned.",
|
"CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.",
|
||||||
"CreateSuccessTitle": "Resource provisioned",
|
"CreateSuccessTitle": "Resource provisioned",
|
||||||
"CreateSuccesseMessage": "Materialized views builder resource provisioned.",
|
"CreateSuccesseMessage": "Materializedviews Builder resource provisioned.",
|
||||||
"CreateFailureTitle": "Failed to provision resource",
|
"CreateFailureTitle": "Failed to provision resource",
|
||||||
"CreateFailureMessage": "Materialized views builder resource provisioning failed.",
|
"CreateFailureMessage": "Materializedviews Builder resource provisioning failed.",
|
||||||
"UpdateMessage": "Materialized views builder resource is being updated.",
|
"UpdateMessage": "MaterializedViewsBuilder resource is being updated.",
|
||||||
"UpdateInitializeTitle": "Updating resource",
|
"UpdateInitializeTitle": "Updating resource",
|
||||||
"UpdateInitializeMessage": "Materialized views builder resource will be updated.",
|
"UpdateInitializeMessage": "Materializedviews Builder resource will be updated.",
|
||||||
"UpdateSuccessTitle": "Resource updated",
|
"UpdateSuccessTitle": "Resource updated",
|
||||||
"UpdateSuccesseMessage": "Materialized views builder resource updated.",
|
"UpdateSuccesseMessage": "Materializedviews Builder resource updated.",
|
||||||
"UpdateFailureTitle": "Failed to update resource",
|
"UpdateFailureTitle": "Failed to update resource",
|
||||||
"UpdateFailureMessage": "Materialized views builder resource update failed.",
|
"UpdateFailureMessage": "Materializedviews Builder resource updation failed.",
|
||||||
"DeleteMessage": "Materialized views builder resource is being deleted.",
|
"DeleteMessage": "MaterializedViewsBuilder resource is being deleted.",
|
||||||
"DeleteInitializeTitle": "Deleting resource",
|
"DeleteInitializeTitle": "Deleting resource",
|
||||||
"DeleteInitializeMessage": "Materialized views builder resource will be deleted.",
|
"DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.",
|
||||||
"DeleteSuccessTitle": "Resource deleted",
|
"DeleteSuccessTitle": "Resource deleted",
|
||||||
"DeleteSuccesseMessage": "Materialized views builder resource deleted.",
|
"DeleteSuccesseMessage": "Materializedviews Builder resource deleted.",
|
||||||
"DeleteFailureTitle": "Failed to delete resource",
|
"DeleteFailureTitle": "Failed to delete resource",
|
||||||
"DeleteFailureMessage": "Materialized views builder resource deletion failed.",
|
"DeleteFailureMessage": "Materializedviews Builder resource deletion failed.",
|
||||||
"ApproximateCost": "Approximate Cost Per Hour",
|
"ApproximateCost": "Approximate Cost Per Hour",
|
||||||
"CostText": "Hourly cost of the materialized views builder resource depends on the SKU selection, number of instances per region, and number of regions.",
|
"CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.",
|
||||||
"MetricsString": "Metrics",
|
"MetricsString": "Metrics",
|
||||||
"MetricsText": "Monitor the CPU and memory usage for the materialized views builder instances in ",
|
"MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ",
|
||||||
"MetricsBlade": "the metrics blade.",
|
"MetricsBlade": "the metrics blade.",
|
||||||
"MonitorUsage": "Monitor Usage",
|
"MonitorUsage": "Monitor Usage",
|
||||||
"ResizingDecisionText": "To understand if the materialized views builder is the right size, ",
|
"ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ",
|
||||||
"ResizingDecisionLink": "learn more about materialized views builder sizing.",
|
"ResizingDecisionLink": "learn more about Materializedviews Builder sizing.",
|
||||||
"WarningBannerOnUpdate": "Adding or modifying materialized views builder instances may affect your bill.",
|
"WarningBannerOnUpdate": "Adding or modifying Materializedviews 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 containers for any updates and applies them on materialized views as per the materialized view definition.",
|
"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."
|
||||||
"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, number of instances per region, and number of regions.",
|
|
||||||
"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 containers for any updates and applies them on global secondary indexes as per their definition."
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
@@ -35,26 +35,21 @@ import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
|||||||
export class PhoenixClient {
|
export class PhoenixClient {
|
||||||
private armResourceId: string;
|
private armResourceId: string;
|
||||||
private containerHealthHandler: NodeJS.Timeout;
|
private containerHealthHandler: NodeJS.Timeout;
|
||||||
private retryOptions: Options = {
|
private retryOptions: promiseRetry.Options = {
|
||||||
retries: Notebook.retryAttempts,
|
retries: Notebook.retryAttempts,
|
||||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||||
minTimeout: Notebook.retryAttemptDelayMs,
|
minTimeout: Notebook.retryAttemptDelayMs,
|
||||||
};
|
};
|
||||||
private abortController: AbortController;
|
|
||||||
private abortSignal: AbortSignal;
|
|
||||||
|
|
||||||
constructor(armResourceId: string) {
|
constructor(armResourceId: string) {
|
||||||
this.armResourceId = armResourceId;
|
this.armResourceId = armResourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||||
this.initializeCancelEventListener();
|
|
||||||
|
|
||||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
||||||
retries: 4,
|
retries: 4,
|
||||||
maxTimeout: 20000,
|
maxTimeout: 20000,
|
||||||
minTimeout: 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 {
|
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
||||||
const errInfo = jsonData;
|
const errInfo = jsonData;
|
||||||
switch (errInfo?.type) {
|
switch (errInfo?.type) {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { RefreshResult } from "../SelfServeTypes";
|
|||||||
import MaterializedViewsBuilder from "./MaterializedViewsBuilder";
|
import MaterializedViewsBuilder from "./MaterializedViewsBuilder";
|
||||||
import {
|
import {
|
||||||
FetchPricesResponse,
|
FetchPricesResponse,
|
||||||
MaterializedViewsBuilderServiceResource,
|
|
||||||
PriceMapAndCurrencyCode,
|
PriceMapAndCurrencyCode,
|
||||||
RegionsResponse,
|
RegionsResponse,
|
||||||
|
MaterializedViewsBuilderServiceResource,
|
||||||
UpdateMaterializedViewsBuilderRequestParameters,
|
UpdateMaterializedViewsBuilderRequestParameters,
|
||||||
} from "./MaterializedViewsBuilderTypes";
|
} from "./MaterializedViewsBuilderTypes";
|
||||||
|
|
||||||
@@ -123,23 +123,11 @@ export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<Ref
|
|||||||
if (response.properties.status === ResourceStatus.Running.toString()) {
|
if (response.properties.status === ResourceStatus.Running.toString()) {
|
||||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||||
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
|
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
|
||||||
return {
|
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
|
||||||
isUpdateInProgress: true,
|
|
||||||
updateInProgressMessageTKey:
|
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateMessage" : "CreateMessage",
|
|
||||||
};
|
|
||||||
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
|
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
|
||||||
return {
|
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
|
||||||
isUpdateInProgress: true,
|
|
||||||
updateInProgressMessageTKey:
|
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteMessage" : "DeleteMessage",
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
|
||||||
isUpdateInProgress: true,
|
|
||||||
updateInProgressMessageTKey:
|
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateMessage" : "UpdateMessage",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
//TODO differentiate between different failures
|
//TODO differentiate between different failures
|
||||||
|
|||||||
@@ -29,20 +29,17 @@ import {
|
|||||||
updateMaterializedViewsBuilderResource,
|
updateMaterializedViewsBuilderResource,
|
||||||
} from "./MaterializedViewsBuilder.rp";
|
} from "./MaterializedViewsBuilder.rp";
|
||||||
|
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
|
|
||||||
const costPerHourDefaultValue: Description = {
|
const costPerHourDefaultValue: Description = {
|
||||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
|
textTKey: "CostText",
|
||||||
type: DescriptionType.Text,
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||||
textTKey:
|
textTKey: "MaterializedviewsBuilderPricing",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const metricsStringValue: Description = {
|
const metricsStringValue: Description = {
|
||||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesMetricsText" : "MetricsText",
|
textTKey: "MetricsText",
|
||||||
type: DescriptionType.Text,
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
href: generateBladeLink(BladeType.Metrics),
|
href: generateBladeLink(BladeType.Metrics),
|
||||||
@@ -79,8 +76,7 @@ const onNumberOfInstancesChange = (
|
|||||||
textTKey: "WarningBannerOnUpdate",
|
textTKey: "WarningBannerOnUpdate",
|
||||||
link: {
|
link: {
|
||||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||||
textTKey:
|
textTKey: "MaterializedviewsBuilderPricing",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
|
||||||
},
|
},
|
||||||
} as Description,
|
} as Description,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
@@ -120,8 +116,7 @@ const onEnableMaterializedViewsBuilderChange = (
|
|||||||
textTKey: "WarningBannerOnUpdate",
|
textTKey: "WarningBannerOnUpdate",
|
||||||
link: {
|
link: {
|
||||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||||
textTKey:
|
textTKey: "MaterializedviewsBuilderPricing",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
|
||||||
},
|
},
|
||||||
} as Description,
|
} as Description,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
@@ -134,17 +129,10 @@ const onEnableMaterializedViewsBuilderChange = (
|
|||||||
} else {
|
} else {
|
||||||
currentValues.set("warningBanner", {
|
currentValues.set("warningBanner", {
|
||||||
value: {
|
value: {
|
||||||
textTKey:
|
textTKey: "WarningBannerOnDelete",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesWarningBannerOnDelete" : "WarningBannerOnDelete",
|
|
||||||
link: {
|
link: {
|
||||||
href:
|
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||||
userContext.apiType === "SQL"
|
textTKey: "DeprovisioningDetailsText",
|
||||||
? "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",
|
|
||||||
},
|
},
|
||||||
} as Description,
|
} as Description,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
@@ -194,19 +182,18 @@ const getInstancesMax = async (): Promise<number> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NumberOfInstancesDropdownInfo: Info = {
|
const NumberOfInstancesDropdownInfo: Info = {
|
||||||
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesResizingDecisionText" : "ResizingDecisionText",
|
messageTKey: "ResizingDecisionText",
|
||||||
link: {
|
link: {
|
||||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
|
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
|
||||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesesizingDecisionLink" : "ResizingDecisionLink",
|
textTKey: "ResizingDecisionLink",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ApproximateCostDropDownInfo: Info = {
|
const ApproximateCostDropDownInfo: Info = {
|
||||||
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
|
messageTKey: "CostText",
|
||||||
link: {
|
link: {
|
||||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||||
textTKey:
|
textTKey: "MaterializedviewsBuilderPricing",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -281,20 +268,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
|||||||
portalNotification: {
|
portalNotification: {
|
||||||
initialize: {
|
initialize: {
|
||||||
titleTKey: "DeleteInitializeTitle",
|
titleTKey: "DeleteInitializeTitle",
|
||||||
messageTKey:
|
messageTKey: "DeleteInitializeMessage",
|
||||||
userContext.apiType === "SQL"
|
|
||||||
? "GlobalsecondaryindexesDeleteInitializeMessage"
|
|
||||||
: "DeleteInitializeMessage",
|
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
titleTKey: "DeleteSuccessTitle",
|
titleTKey: "DeleteSuccessTitle",
|
||||||
messageTKey:
|
messageTKey: "DeleteSuccesseMessage",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteSuccesseMessage" : "DeleteSuccesseMessage",
|
|
||||||
},
|
},
|
||||||
failure: {
|
failure: {
|
||||||
titleTKey: "DeleteFailureTitle",
|
titleTKey: "DeleteFailureTitle",
|
||||||
messageTKey:
|
messageTKey: "DeleteFailureMessage",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteFailureMessage" : "DeleteFailureMessage",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -307,20 +289,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
|||||||
portalNotification: {
|
portalNotification: {
|
||||||
initialize: {
|
initialize: {
|
||||||
titleTKey: "UpdateInitializeTitle",
|
titleTKey: "UpdateInitializeTitle",
|
||||||
messageTKey:
|
messageTKey: "UpdateInitializeMessage",
|
||||||
userContext.apiType === "SQL"
|
|
||||||
? "GlobalsecondaryindexesUpdateInitializeMessage"
|
|
||||||
: "UpdateInitializeMessage",
|
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
titleTKey: "UpdateSuccessTitle",
|
titleTKey: "UpdateSuccessTitle",
|
||||||
messageTKey:
|
messageTKey: "UpdateSuccesseMessage",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateSuccesseMessage" : "UpdateSuccesseMessage",
|
|
||||||
},
|
},
|
||||||
failure: {
|
failure: {
|
||||||
titleTKey: "UpdateFailureTitle",
|
titleTKey: "UpdateFailureTitle",
|
||||||
messageTKey:
|
messageTKey: "UpdateFailureMessage",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateFailureMessage" : "UpdateFailureMessage",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -334,20 +311,15 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
|||||||
portalNotification: {
|
portalNotification: {
|
||||||
initialize: {
|
initialize: {
|
||||||
titleTKey: "CreateInitializeTitle",
|
titleTKey: "CreateInitializeTitle",
|
||||||
messageTKey:
|
messageTKey: "CreateInitializeMessage",
|
||||||
userContext.apiType === "SQL"
|
|
||||||
? "GlobalsecondaryindexesCreateInitializeMessage"
|
|
||||||
: "CreateInitializeMessage",
|
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
titleTKey: "CreateSuccessTitle",
|
titleTKey: "CreateSuccessTitle",
|
||||||
messageTKey:
|
messageTKey: "CreateSuccesseMessage",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateSuccesseMessage" : "CreateSuccesseMessage",
|
|
||||||
},
|
},
|
||||||
failure: {
|
failure: {
|
||||||
titleTKey: "CreateFailureTitle",
|
titleTKey: "CreateFailureTitle",
|
||||||
messageTKey:
|
messageTKey: "CreateFailureMessage",
|
||||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateFailureMessage" : "CreateFailureMessage",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -394,17 +366,11 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
|||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
description: {
|
description: {
|
||||||
textTKey:
|
textTKey: "MaterializedViewsBuilderDescription",
|
||||||
userContext.apiType === "SQL"
|
|
||||||
? "GlobalsecondaryindexesBuilderDescription"
|
|
||||||
: "MaterializedViewsBuilderDescription",
|
|
||||||
type: DescriptionType.Text,
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
href:
|
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||||
userContext.apiType === "SQL"
|
textTKey: "LearnAboutMaterializedViews",
|
||||||
? "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",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -412,7 +378,7 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
|||||||
|
|
||||||
@OnChange(onEnableMaterializedViewsBuilderChange)
|
@OnChange(onEnableMaterializedViewsBuilderChange)
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: userContext.apiType === "SQL" ? "GlobalSecondaryIndexesBuilder" : "MaterializedViewsBuilder",
|
labelTKey: "MaterializedViewsBuilder",
|
||||||
trueLabelTKey: "Provisioned",
|
trueLabelTKey: "Provisioned",
|
||||||
falseLabelTKey: "Deprovisioned",
|
falseLabelTKey: "Deprovisioned",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,24 +11,13 @@ import { updateUserContext } from "../UserContext";
|
|||||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||||
import "./SelfServe.less";
|
import "./SelfServe.less";
|
||||||
import { SelfServeComponent } from "./SelfServeComponent";
|
import { SelfServeComponent } from "./SelfServeComponent";
|
||||||
import { SelfServeBaseClass, SelfServeDescriptor } from "./SelfServeTypes";
|
import { SelfServeDescriptor } from "./SelfServeTypes";
|
||||||
import { SelfServeType } from "./SelfServeUtils";
|
import { SelfServeType } from "./SelfServeUtils";
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
const loadTranslationFile = async (
|
const loadTranslationFile = async (className: string): Promise<void> => {
|
||||||
className: string | SelfServeBaseClass,
|
|
||||||
selfServeType?: SelfServeType,
|
|
||||||
): Promise<void> => {
|
|
||||||
const language = i18n.languages[0];
|
const language = i18n.languages[0];
|
||||||
let namespace: string; // className is used as a key to retrieve the localized strings
|
const fileName = `${className}.json`;
|
||||||
let fileName: string;
|
|
||||||
if (className instanceof SelfServeBaseClass) {
|
|
||||||
fileName = `${selfServeType}.json`;
|
|
||||||
namespace = className.constructor.name;
|
|
||||||
} else {
|
|
||||||
fileName = `${className}.json`;
|
|
||||||
namespace = className;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let translations: any;
|
let translations: any;
|
||||||
@@ -39,16 +28,12 @@ const loadTranslationFile = async (
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`);
|
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`);
|
||||||
}
|
}
|
||||||
|
i18n.addResourceBundle(language, className, translations.default, true);
|
||||||
i18n.addResourceBundle(language, namespace, translations.default, true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadTranslations = async (
|
const loadTranslations = async (className: string): Promise<void> => {
|
||||||
className: string | SelfServeBaseClass,
|
|
||||||
selfServeType: SelfServeType,
|
|
||||||
): Promise<void> => {
|
|
||||||
await loadTranslationFile("Common");
|
await loadTranslationFile("Common");
|
||||||
await loadTranslationFile(className, selfServeType);
|
await loadTranslationFile(className);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
||||||
@@ -56,13 +41,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
case SelfServeType.example: {
|
case SelfServeType.example: {
|
||||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||||
const selfServeExample = new SelfServeExample.default();
|
const selfServeExample = new SelfServeExample.default();
|
||||||
await loadTranslations(selfServeExample, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return selfServeExample.toSelfServeDescriptor();
|
return selfServeExample.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.sqlx: {
|
case SelfServeType.sqlx: {
|
||||||
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||||
const sqlX = new SqlX.default();
|
const sqlX = new SqlX.default();
|
||||||
await loadTranslations(sqlX, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return sqlX.toSelfServeDescriptor();
|
return sqlX.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.graphapicompute: {
|
case SelfServeType.graphapicompute: {
|
||||||
@@ -70,7 +55,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||||
);
|
);
|
||||||
const graphAPICompute = new GraphAPICompute.default();
|
const graphAPICompute = new GraphAPICompute.default();
|
||||||
await loadTranslations(graphAPICompute, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return graphAPICompute.toSelfServeDescriptor();
|
return graphAPICompute.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
case SelfServeType.materializedviewsbuilder: {
|
case SelfServeType.materializedviewsbuilder: {
|
||||||
@@ -78,7 +63,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||||
);
|
);
|
||||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||||
await loadTranslations(materializedViewsBuilder, selfServeType);
|
await loadTranslations(selfServeType);
|
||||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { WithTranslation } from "react-i18next";
|
import { WithTranslation } from "react-i18next";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
@@ -80,7 +80,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
private static readonly defaultRetryIntervalInMs = 30000;
|
private static readonly defaultRetryIntervalInMs = 30000;
|
||||||
private smartUiGeneratorClassName: string;
|
private smartUiGeneratorClassName: string;
|
||||||
private retryIntervalInMs: number;
|
private retryIntervalInMs: number;
|
||||||
private retryOptions: Options;
|
private retryOptions: promiseRetry.Options;
|
||||||
private translationFunction: TFunction;
|
private translationFunction: TFunction;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
|||||||
@@ -197,11 +197,6 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
|||||||
const priceMap = new Map<string, Map<string, number>>();
|
const priceMap = new Map<string, Map<string, number>>();
|
||||||
let billingCurrency;
|
let billingCurrency;
|
||||||
for (const region of map.keys()) {
|
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 regionPriceMap = new Map<string, number>();
|
||||||
const regionShortName = await getRegionShortName(region);
|
const regionShortName = await getRegionShortName(region);
|
||||||
const requestBody: OfferingIdRequest = {
|
const requestBody: OfferingIdRequest = {
|
||||||
@@ -242,7 +237,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
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) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||||
return new Map<string, Map<string, string>>();
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -227,13 +227,11 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
|||||||
let costPerHour = 0;
|
let costPerHour = 0;
|
||||||
let costBreakdown = "";
|
let costBreakdown = "";
|
||||||
for (const regionItem of regions) {
|
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) {
|
if (incrementalCost === undefined) {
|
||||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||||
} else if (incrementalCost === 0) {
|
} else if (incrementalCost === 0) {
|
||||||
throw new Error(`${regionItem.locationName} cost per hour = 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;
|
let regionalInstanceCount = instanceCount;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export enum AppStateComponentNames {
|
|||||||
MostRecentActivity = "MostRecentActivity",
|
MostRecentActivity = "MostRecentActivity",
|
||||||
QueryCopilot = "QueryCopilot",
|
QueryCopilot = "QueryCopilot",
|
||||||
DataExplorerAction = "DataExplorerAction",
|
DataExplorerAction = "DataExplorerAction",
|
||||||
SelectedRegionalEndpoint = "SelectedRegionalEndpoint",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subcomponent for DataExplorerAction
|
// Subcomponent for DataExplorerAction
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
// Data Explorer specific actions. No need to keep this in sync with the one in Portal.
|
// Data Explorer specific actions. No need to keep this in sync with the one in Portal.
|
||||||
// Some of the enums names are used in Fabric. Please do not rename them.
|
|
||||||
export enum Action {
|
export enum Action {
|
||||||
CollapseTreeNode,
|
CollapseTreeNode,
|
||||||
CreateCollection, // Used in Fabric. Please do not rename.
|
CreateCollection,
|
||||||
CreateGlobalSecondaryIndex,
|
CreateMaterializedView,
|
||||||
CreateDocument, // Used in Fabric. Please do not rename.
|
CreateDocument,
|
||||||
CreateStoredProcedure,
|
CreateStoredProcedure,
|
||||||
CreateTrigger,
|
CreateTrigger,
|
||||||
CreateUDF,
|
CreateUDF,
|
||||||
DeleteCollection, // Used in Fabric. Please do not rename.
|
DeleteCollection,
|
||||||
DeleteDatabase,
|
DeleteDatabase,
|
||||||
DeleteDocument,
|
DeleteDocument,
|
||||||
ExpandTreeNode,
|
ExpandTreeNode,
|
||||||
ExecuteQuery, // Used in Fabric. Please do not rename.
|
ExecuteQuery,
|
||||||
HasFeature,
|
HasFeature,
|
||||||
GetVNETServices,
|
GetVNETServices,
|
||||||
InitializeAccountLocationFromResourceGroup,
|
InitializeAccountLocationFromResourceGroup,
|
||||||
@@ -121,7 +120,7 @@ export enum Action {
|
|||||||
NotebooksGalleryPublishedCount,
|
NotebooksGalleryPublishedCount,
|
||||||
SelfServe,
|
SelfServe,
|
||||||
ExpandAddCollectionPaneAdvancedSection,
|
ExpandAddCollectionPaneAdvancedSection,
|
||||||
ExpandAddGlobalSecondaryIndexPaneAdvancedSection,
|
ExpandAddMaterializedViewPaneAdvancedSection,
|
||||||
SchemaAnalyzerClickAnalyze,
|
SchemaAnalyzerClickAnalyze,
|
||||||
SelfServeComponent,
|
SelfServeComponent,
|
||||||
LaunchQuickstart,
|
LaunchQuickstart,
|
||||||
@@ -145,7 +144,6 @@ export enum Action {
|
|||||||
ReadPersistedTabState,
|
ReadPersistedTabState,
|
||||||
SavePersistedTabState,
|
SavePersistedTabState,
|
||||||
DeletePersistedTabState,
|
DeletePersistedTabState,
|
||||||
UploadDocuments, // Used in Fabric. Please do not rename.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class JupyterLabAppFactory {
|
|||||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
||||||
this.restartShell = true;
|
this.restartShell = true;
|
||||||
}
|
}
|
||||||
return content?.includes("cosmosshelluser@");
|
return content?.includes("cosmosuser@");
|
||||||
}
|
}
|
||||||
|
|
||||||
private isMongoShellStarted(content: string | undefined) {
|
private isMongoShellStarted(content: string | undefined) {
|
||||||
@@ -68,6 +68,7 @@ export class JupyterLabAppFactory {
|
|||||||
const session = await manager.startNew();
|
const session = await manager.startNew();
|
||||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||||
const content = message.content && message.content[0]?.toString();
|
const content = message.content && message.content[0]?.toString();
|
||||||
|
|
||||||
if (this.checkShellStarted && message.type == "stdout") {
|
if (this.checkShellStarted && message.type == "stdout") {
|
||||||
//Close the terminal tab once the shell closed messages are received
|
//Close the terminal tab once the shell closed messages are received
|
||||||
if (!this.isShellStarted) {
|
if (!this.isShellStarted) {
|
||||||
@@ -113,13 +114,6 @@ export class JupyterLabAppFactory {
|
|||||||
panel.dispose();
|
panel.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close terminal when Ctrl key is pressed
|
|
||||||
term.node.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
this.onShellExited(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,6 @@ export interface UserContext {
|
|||||||
readonly isReplica?: boolean;
|
readonly isReplica?: boolean;
|
||||||
collectionCreationDefaults: CollectionCreationDefaults;
|
collectionCreationDefaults: CollectionCreationDefaults;
|
||||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||||
readonly selectedRegionalEndpoint?: string;
|
|
||||||
readonly writeEnabledInSelectedRegion?: boolean;
|
|
||||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||||
readonly dataPlaneRbacEnabled?: boolean;
|
readonly dataPlaneRbacEnabled?: boolean;
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ describe("AuthorizationUtils", () => {
|
|||||||
it("should throw an error if token is malformed", () => {
|
it("should throw an error if token is malformed", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
AuthorizationUtils.decryptJWTToken(
|
AuthorizationUtils.decryptJWTToken(
|
||||||
// This is an invalid JWT token used for testing
|
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.",
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.",
|
||||||
),
|
),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
@@ -48,7 +47,6 @@ describe("AuthorizationUtils", () => {
|
|||||||
it("should return decrypted token payload", () => {
|
it("should return decrypted token payload", () => {
|
||||||
expect(
|
expect(
|
||||||
AuthorizationUtils.decryptJWTToken(
|
AuthorizationUtils.decryptJWTToken(
|
||||||
// This is an expired JWT token used for testing
|
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ",
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ",
|
||||||
),
|
),
|
||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
|
|||||||
@@ -35,13 +35,6 @@ describe("Query Utils", () => {
|
|||||||
version: 2,
|
version: 2,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const generatePartitionKeysForPaths = (paths: string[]): DataModels.PartitionKey => {
|
|
||||||
return {
|
|
||||||
paths: paths,
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("buildDocumentsQueryPartitionProjections()", () => {
|
describe("buildDocumentsQueryPartitionProjections()", () => {
|
||||||
it("should return empty string if partition key is undefined", () => {
|
it("should return empty string if partition key is undefined", () => {
|
||||||
@@ -96,18 +89,6 @@ describe("Query Utils", () => {
|
|||||||
|
|
||||||
expect(query).toContain("c.id");
|
expect(query).toContain("c.id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should always include {} for any missing partition keys", () => {
|
|
||||||
const query = QueryUtils.buildDocumentsQuery(
|
|
||||||
"",
|
|
||||||
["a", "b", "c"],
|
|
||||||
generatePartitionKeysForPaths(["/a", "/b", "/c"]),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
expect(query).toContain('IIF(IS_DEFINED(c["a"]), c["a"], {})');
|
|
||||||
expect(query).toContain('IIF(IS_DEFINED(c["b"]), c["b"], {})');
|
|
||||||
expect(query).toContain('IIF(IS_DEFINED(c["c"]), c["c"], {})');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("queryPagesUntilContentPresent()", () => {
|
describe("queryPagesUntilContentPresent()", () => {
|
||||||
@@ -220,6 +201,18 @@ describe("Query Utils", () => {
|
|||||||
expect(expectedPartitionKeyValues).toContain(documentContent["Category"]);
|
expect(expectedPartitionKeyValues).toContain(documentContent["Category"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should extract no partition key values in the case nested partition key", () => {
|
||||||
|
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
|
kind: PartitionKeyKind.Hash,
|
||||||
|
paths: ["/Location.type"],
|
||||||
|
};
|
||||||
|
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
||||||
|
documentContent,
|
||||||
|
singlePartitionKeyDefinition,
|
||||||
|
);
|
||||||
|
expect(partitionKeyValues.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("should extract all partition key values for hierarchical and nested partition keys", () => {
|
it("should extract all partition key values for hierarchical and nested partition keys", () => {
|
||||||
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
|
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
kind: PartitionKeyKind.MultiHash,
|
kind: PartitionKeyKind.MultiHash,
|
||||||
@@ -232,52 +225,5 @@ describe("Query Utils", () => {
|
|||||||
expect(partitionKeyValues.length).toBe(2);
|
expect(partitionKeyValues.length).toBe(2);
|
||||||
expect(partitionKeyValues).toEqual(["United States", "Point"]);
|
expect(partitionKeyValues).toEqual(["United States", "Point"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("if any partition key is null or empty string, the partitionKeyValues shall match", () => {
|
|
||||||
const newDocumentContent = {
|
|
||||||
...documentContent,
|
|
||||||
...{
|
|
||||||
Country: null,
|
|
||||||
Location: {
|
|
||||||
type: "",
|
|
||||||
coordinates: [-121.49, 46.206],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
|
|
||||||
kind: PartitionKeyKind.MultiHash,
|
|
||||||
paths: ["/Country", "/Location/type"],
|
|
||||||
};
|
|
||||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
newDocumentContent,
|
|
||||||
mixedPartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
expect(partitionKeyValues.length).toBe(2);
|
|
||||||
expect(partitionKeyValues).toEqual([null, ""]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("if any partition key doesn't exist, it should still set partitionkey value as {}", () => {
|
|
||||||
const newDocumentContent = {
|
|
||||||
...documentContent,
|
|
||||||
...{
|
|
||||||
Country: null,
|
|
||||||
Location: {
|
|
||||||
coordinates: [-121.49, 46.206],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
|
|
||||||
kind: PartitionKeyKind.MultiHash,
|
|
||||||
paths: ["/Country", "/Location/type"],
|
|
||||||
};
|
|
||||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
newDocumentContent,
|
|
||||||
mixedPartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
expect(partitionKeyValues.length).toBe(2);
|
|
||||||
expect(partitionKeyValues).toEqual([null, {}]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export function buildDocumentsQueryPartitionProjections(
|
|||||||
for (const index in partitionKey.paths) {
|
for (const index in partitionKey.paths) {
|
||||||
// TODO: Handle "/" in partition key definitions
|
// TODO: Handle "/" in partition key definitions
|
||||||
const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1);
|
const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1);
|
||||||
const isSystemPartitionKey: boolean = partitionKey.systemKey || false;
|
|
||||||
let projectedProperty = "";
|
let projectedProperty = "";
|
||||||
|
|
||||||
projectedProperties.forEach((property: string) => {
|
projectedProperties.forEach((property: string) => {
|
||||||
@@ -62,13 +61,8 @@ export function buildDocumentsQueryPartitionProjections(
|
|||||||
projectedProperty += `[${projection}]`;
|
projectedProperty += `[${projection}]`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const fullAccess = `${collectionAlias}${projectedProperty}`;
|
|
||||||
if (!isSystemPartitionKey) {
|
projections.push(`${collectionAlias}${projectedProperty}`);
|
||||||
const wrappedProjection = `IIF(IS_DEFINED(${fullAccess}), ${fullAccess}, {})`;
|
|
||||||
projections.push(wrappedProjection);
|
|
||||||
} else {
|
|
||||||
projections.push(fullAccess);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return projections.join(",");
|
return projections.join(",");
|
||||||
@@ -124,7 +118,7 @@ export const extractPartitionKeyValues = (
|
|||||||
documentContent: any,
|
documentContent: any,
|
||||||
partitionKeyDefinition: PartitionKeyDefinition,
|
partitionKeyDefinition: PartitionKeyDefinition,
|
||||||
): PartitionKey[] => {
|
): PartitionKey[] => {
|
||||||
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0 || partitionKeyDefinition.systemKey) {
|
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,8 +130,6 @@ export const extractPartitionKeyValues = (
|
|||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
partitionKeyValues.push(value);
|
partitionKeyValues.push(value);
|
||||||
} else {
|
|
||||||
partitionKeyValues.push({});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import create, { UseStore } from "zustand";
|
|
||||||
interface ClientWriteEnabledState {
|
|
||||||
clientWriteEnabled: boolean;
|
|
||||||
setClientWriteEnabled: (writeEnabled: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useClientWriteEnabled: UseStore<ClientWriteEnabledState> = create((set) => ({
|
|
||||||
clientWriteEnabled: true,
|
|
||||||
setClientWriteEnabled: (clientWriteEnabled: boolean) => set({ clientWriteEnabled }),
|
|
||||||
}));
|
|
||||||
@@ -17,16 +17,12 @@ import { useSelectedNode } from "Explorer/useSelectedNode";
|
|||||||
import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
||||||
import {
|
import {
|
||||||
AppStateComponentNames,
|
AppStateComponentNames,
|
||||||
deleteState,
|
|
||||||
hasState,
|
|
||||||
loadState,
|
|
||||||
OPEN_TABS_SUBCOMPONENT_NAME,
|
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||||
readSubComponentState,
|
readSubComponentState,
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -215,10 +211,6 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "refreshResourceTree": {
|
|
||||||
explorer.onRefreshResourcesClick();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown Fabric message type: ${JSON.stringify(data)}`);
|
console.error(`Unknown Fabric message type: ${JSON.stringify(data)}`);
|
||||||
break;
|
break;
|
||||||
@@ -353,9 +345,6 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
|||||||
`Configuring Data Explorer for ${userContext.apiType} account ${account.name}`,
|
`Configuring Data Explorer for ${userContext.apiType} account ${account.name}`,
|
||||||
"Explorer/configureHostedWithAAD",
|
"Explorer/configureHostedWithAAD",
|
||||||
);
|
);
|
||||||
if (userContext.apiType === "SQL") {
|
|
||||||
checkAndUpdateSelectedRegionalEndpoint();
|
|
||||||
}
|
|
||||||
if (!userContext.features.enableAadDataPlane) {
|
if (!userContext.features.enableAadDataPlane) {
|
||||||
Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD");
|
Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD");
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
@@ -717,10 +706,6 @@ async function configurePortal(): Promise<Explorer> {
|
|||||||
|
|
||||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||||
|
|
||||||
if (userContext.apiType === "SQL") {
|
|
||||||
checkAndUpdateSelectedRegionalEndpoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataPlaneRbacEnabled;
|
let dataPlaneRbacEnabled;
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) {
|
||||||
@@ -839,41 +824,6 @@ function updateAADEndpoints(portalEnv: PortalEnv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkAndUpdateSelectedRegionalEndpoint() {
|
|
||||||
const accountName = userContext.databaseAccount?.name;
|
|
||||||
if (hasState({ componentName: AppStateComponentNames.SelectedRegionalEndpoint, globalAccountName: accountName })) {
|
|
||||||
const storedRegionalEndpoint = loadState({
|
|
||||||
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
}) as string;
|
|
||||||
const validEndpoint = userContext.databaseAccount?.properties?.readLocations?.find(
|
|
||||||
(loc) => loc.documentEndpoint === storedRegionalEndpoint,
|
|
||||||
);
|
|
||||||
const validWriteEndpoint = userContext.databaseAccount?.properties?.writeLocations?.find(
|
|
||||||
(loc) => loc.documentEndpoint === storedRegionalEndpoint,
|
|
||||||
);
|
|
||||||
if (validEndpoint) {
|
|
||||||
updateUserContext({
|
|
||||||
selectedRegionalEndpoint: storedRegionalEndpoint,
|
|
||||||
writeEnabledInSelectedRegion: !!validWriteEndpoint,
|
|
||||||
refreshCosmosClient: true,
|
|
||||||
});
|
|
||||||
useClientWriteEnabled.setState({ clientWriteEnabled: !!validWriteEndpoint });
|
|
||||||
} else {
|
|
||||||
deleteState({ componentName: AppStateComponentNames.SelectedRegionalEndpoint, globalAccountName: accountName });
|
|
||||||
updateUserContext({
|
|
||||||
writeEnabledInSelectedRegion: true,
|
|
||||||
});
|
|
||||||
useClientWriteEnabled.setState({ clientWriteEnabled: true });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateUserContext({
|
|
||||||
writeEnabledInSelectedRegion: true,
|
|
||||||
});
|
|
||||||
useClientWriteEnabled.setState({ clientWriteEnabled: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||||
if (
|
if (
|
||||||
configContext.PORTAL_BACKEND_ENDPOINT &&
|
configContext.PORTAL_BACKEND_ENDPOINT &&
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
set({ activeTab: undefined, activeReactTab: undefined });
|
set({ activeTab: undefined, activeReactTab: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab.tabId === activeTab?.tabId && tabIndex !== -1) {
|
if (tab.tabId === activeTab.tabId && tabIndex !== -1) {
|
||||||
const tabToTheRight = updatedTabs[tabIndex];
|
const tabToTheRight = updatedTabs[tabIndex];
|
||||||
const lastOpenTab = updatedTabs[updatedTabs.length - 1];
|
const lastOpenTab = updatedTabs[updatedTabs.length - 1];
|
||||||
const newActiveTab = tabToTheRight ?? lastOpenTab;
|
const newActiveTab = tabToTheRight ?? lastOpenTab;
|
||||||
|
|||||||
57
test/fx.ts
57
test/fx.ts
@@ -1,5 +1,5 @@
|
|||||||
import { AzureCliCredential } from "@azure/identity";
|
import { AzureCliCredential } from "@azure/identity";
|
||||||
import { Frame, Locator, Page, expect } from "@playwright/test";
|
import { expect, Frame, Locator, Page } from "@playwright/test";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
const RETRY_COUNT = 3;
|
const RETRY_COUNT = 3;
|
||||||
@@ -26,7 +26,7 @@ export function getAzureCLICredentials(): AzureCliCredential {
|
|||||||
|
|
||||||
export async function getAzureCLICredentialsToken(): Promise<string> {
|
export async function getAzureCLICredentialsToken(): Promise<string> {
|
||||||
const credentials = getAzureCLICredentials();
|
const credentials = getAzureCLICredentials();
|
||||||
const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || "";
|
const token = (await credentials.getToken("https://management.core.windows.net//.default")).token;
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +35,8 @@ export enum TestAccount {
|
|||||||
Cassandra = "Cassandra",
|
Cassandra = "Cassandra",
|
||||||
Gremlin = "Gremlin",
|
Gremlin = "Gremlin",
|
||||||
Mongo = "Mongo",
|
Mongo = "Mongo",
|
||||||
MongoReadonly = "MongoReadOnly",
|
|
||||||
Mongo32 = "Mongo32",
|
Mongo32 = "Mongo32",
|
||||||
SQL = "SQL",
|
SQL = "SQL",
|
||||||
SQLReadOnly = "SQLReadOnly",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAccounts: Record<TestAccount, string> = {
|
export const defaultAccounts: Record<TestAccount, string> = {
|
||||||
@@ -46,10 +44,8 @@ export const defaultAccounts: Record<TestAccount, string> = {
|
|||||||
[TestAccount.Cassandra]: "github-e2etests-cassandra",
|
[TestAccount.Cassandra]: "github-e2etests-cassandra",
|
||||||
[TestAccount.Gremlin]: "github-e2etests-gremlin",
|
[TestAccount.Gremlin]: "github-e2etests-gremlin",
|
||||||
[TestAccount.Mongo]: "github-e2etests-mongo",
|
[TestAccount.Mongo]: "github-e2etests-mongo",
|
||||||
[TestAccount.MongoReadonly]: "github-e2etests-mongo-readonly",
|
|
||||||
[TestAccount.Mongo32]: "github-e2etests-mongo32",
|
[TestAccount.Mongo32]: "github-e2etests-mongo32",
|
||||||
[TestAccount.SQL]: "github-e2etests-sql",
|
[TestAccount.SQL]: "github-e2etests-sql",
|
||||||
[TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
||||||
@@ -218,25 +214,6 @@ export class QueryTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DocumentsTab {
|
|
||||||
documentsFilter: Locator;
|
|
||||||
documentsListPane: Locator;
|
|
||||||
documentResultsPane: Locator;
|
|
||||||
resultsEditor: Editor;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public frame: Frame,
|
|
||||||
public tabId: string,
|
|
||||||
public tab: Locator,
|
|
||||||
public locator: Locator,
|
|
||||||
) {
|
|
||||||
this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter");
|
|
||||||
this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane");
|
|
||||||
this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane");
|
|
||||||
this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PanelOpenOptions = {
|
type PanelOpenOptions = {
|
||||||
closeTimeout?: number;
|
closeTimeout?: number;
|
||||||
};
|
};
|
||||||
@@ -255,12 +232,6 @@ export class DataExplorer {
|
|||||||
return new QueryTab(this.frame, tabId, tab, queryTab);
|
return new QueryTab(this.frame, tabId, tab, queryTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
documentsTab(tabId: string): DocumentsTab {
|
|
||||||
const tab = this.tab(tabId);
|
|
||||||
const documentsTab = tab.getByTestId("DocumentsTab");
|
|
||||||
return new DocumentsTab(this.frame, tabId, tab, documentsTab);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Select the primary global command button.
|
/** Select the primary global command button.
|
||||||
*
|
*
|
||||||
* There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button.
|
* There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button.
|
||||||
@@ -274,10 +245,6 @@ export class DataExplorer {
|
|||||||
return this.frame.getByTestId(`CommandBar/Button:${label}`).and(this.frame.locator("css=button"));
|
return this.frame.getByTestId(`CommandBar/Button:${label}`).and(this.frame.locator("css=button"));
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogButton(label: string): Locator {
|
|
||||||
return this.frame.getByTestId(`DialogButton:${label}`).and(this.frame.locator("css=button"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Select the side panel with the specified title */
|
/** Select the side panel with the specified title */
|
||||||
panel(title: string): Locator {
|
panel(title: string): Locator {
|
||||||
return this.frame.getByTestId(`Panel:${title}`);
|
return this.frame.getByTestId(`Panel:${title}`);
|
||||||
@@ -327,26 +294,6 @@ export class DataExplorer {
|
|||||||
return await this.waitForNode(`${databaseId}/${containerId}`);
|
return await this.waitForNode(`${databaseId}/${containerId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForContainerItemsNode(databaseId: string, containerId: string): Promise<TreeNode> {
|
|
||||||
return await this.waitForNode(`${databaseId}/${containerId}/Items`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForContainerDocumentsNode(databaseId: string, containerId: string): Promise<TreeNode> {
|
|
||||||
return await this.waitForNode(`${databaseId}/${containerId}/Documents`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForCommandBarButton(label: string, timeout?: number): Promise<Locator> {
|
|
||||||
const commandBar = this.commandBarButton(label);
|
|
||||||
await commandBar.waitFor({ state: "visible", timeout });
|
|
||||||
return commandBar;
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForDialogButton(label: string, timeout?: number): Promise<Locator> {
|
|
||||||
const dialogButton = this.dialogButton(label);
|
|
||||||
await dialogButton.waitFor({ timeout });
|
|
||||||
return dialogButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Select the tree node with the specified id */
|
/** Select the tree node with the specified id */
|
||||||
treeNode(id: string): TreeNode {
|
treeNode(id: string): TreeNode {
|
||||||
return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id);
|
return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id);
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
|
||||||
|
|
||||||
import { DataExplorer, DocumentsTab, TestAccount } from "../fx";
|
|
||||||
import { retry, serializeMongoToJson, setPartitionKeys } from "../testData";
|
|
||||||
import { documentTestCases } from "./testCases";
|
|
||||||
|
|
||||||
let explorer: DataExplorer = null!;
|
|
||||||
let documentsTab: DocumentsTab = null!;
|
|
||||||
|
|
||||||
for (const { name, databaseId, containerId, documents } of documentTestCases) {
|
|
||||||
test.describe(`Test MongoRU Documents with ${name}`, () => {
|
|
||||||
test.beforeEach("Open documents tab", async ({ page }) => {
|
|
||||||
explorer = await DataExplorer.open(page, TestAccount.MongoReadonly);
|
|
||||||
|
|
||||||
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
|
|
||||||
await containerNode.expand();
|
|
||||||
|
|
||||||
const containerMenuNode = await explorer.waitForContainerDocumentsNode(databaseId, containerId);
|
|
||||||
await containerMenuNode.element.click();
|
|
||||||
|
|
||||||
documentsTab = explorer.documentsTab("tab0");
|
|
||||||
|
|
||||||
await documentsTab.documentsFilter.waitFor();
|
|
||||||
await documentsTab.documentsListPane.waitFor();
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const document of documents) {
|
|
||||||
const { documentId: docId, partitionKeys } = document;
|
|
||||||
test.describe(`Document ID: ${docId}`, () => {
|
|
||||||
test(`should load and view document ${docId}`, async () => {
|
|
||||||
const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0);
|
|
||||||
await span.waitFor();
|
|
||||||
await expect(span).toBeVisible();
|
|
||||||
|
|
||||||
await span.click();
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
|
|
||||||
const resultText = await documentsTab.resultsEditor.text();
|
|
||||||
const resultData = serializeMongoToJson(resultText!);
|
|
||||||
expect(resultText).not.toBeNull();
|
|
||||||
expect(resultData?._id).not.toBeNull();
|
|
||||||
expect(resultData?._id).toEqual(docId);
|
|
||||||
});
|
|
||||||
test(`should be able to create and delete new document from ${docId}`, async () => {
|
|
||||||
const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0);
|
|
||||||
await span.waitFor();
|
|
||||||
await expect(span).toBeVisible();
|
|
||||||
|
|
||||||
await span.click();
|
|
||||||
let newDocumentId;
|
|
||||||
await retry(async () => {
|
|
||||||
const newDocumentButton = await explorer.waitForCommandBarButton("New Document", 5000);
|
|
||||||
await expect(newDocumentButton).toBeVisible();
|
|
||||||
await expect(newDocumentButton).toBeEnabled();
|
|
||||||
await newDocumentButton.click();
|
|
||||||
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
|
|
||||||
newDocumentId = `${Date.now().toString()}-delete`;
|
|
||||||
|
|
||||||
const newDocument = {
|
|
||||||
_id: newDocumentId,
|
|
||||||
...setPartitionKeys(partitionKeys || []),
|
|
||||||
};
|
|
||||||
|
|
||||||
await documentsTab.resultsEditor.setText(JSON.stringify(newDocument));
|
|
||||||
const saveButton = await explorer.waitForCommandBarButton("Save", 5000);
|
|
||||||
await saveButton.click({ timeout: 5000 });
|
|
||||||
}, 3);
|
|
||||||
|
|
||||||
const newSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0);
|
|
||||||
await newSpan.waitFor();
|
|
||||||
await newSpan.click();
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
|
|
||||||
const deleteButton = await explorer.waitForCommandBarButton("Delete", 5000);
|
|
||||||
await deleteButton.click();
|
|
||||||
|
|
||||||
const deleteDialogButton = await explorer.waitForDialogButton("Delete", 5000);
|
|
||||||
await deleteDialogButton.click();
|
|
||||||
|
|
||||||
const deletedSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0);
|
|
||||||
await expect(deletedSpan).toHaveCount(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { DocumentTestCase } from "../testData";
|
|
||||||
|
|
||||||
export const documentTestCases: DocumentTestCase[] = [
|
|
||||||
{
|
|
||||||
name: "Unsharded Collection",
|
|
||||||
databaseId: "e2etests-mongo-readonly",
|
|
||||||
containerId: "unsharded",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "unsharded",
|
|
||||||
partitionKeys: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sharded Collection",
|
|
||||||
databaseId: "e2etests-mongo-readonly",
|
|
||||||
containerId: "sharded",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "sharded",
|
|
||||||
partitionKeys: [
|
|
||||||
{
|
|
||||||
key: "/shardKey",
|
|
||||||
value: "shardKey",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
|
||||||
|
|
||||||
import { DataExplorer, DocumentsTab, TestAccount } from "../fx";
|
|
||||||
import { retry, setPartitionKeys } from "../testData";
|
|
||||||
import { documentTestCases } from "./testCases";
|
|
||||||
|
|
||||||
let explorer: DataExplorer = null!;
|
|
||||||
let documentsTab: DocumentsTab = null!;
|
|
||||||
|
|
||||||
for (const { name, databaseId, containerId, documents } of documentTestCases) {
|
|
||||||
test.describe(`Test SQL Documents with ${name}`, () => {
|
|
||||||
test.beforeEach("Open documents tab", async ({ page }) => {
|
|
||||||
explorer = await DataExplorer.open(page, TestAccount.SQLReadOnly);
|
|
||||||
|
|
||||||
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
|
|
||||||
await containerNode.expand();
|
|
||||||
|
|
||||||
const containerMenuNode = await explorer.waitForContainerItemsNode(databaseId, containerId);
|
|
||||||
await containerMenuNode.element.click();
|
|
||||||
|
|
||||||
documentsTab = explorer.documentsTab("tab0");
|
|
||||||
|
|
||||||
await documentsTab.documentsFilter.waitFor();
|
|
||||||
await documentsTab.documentsListPane.waitFor();
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const document of documents) {
|
|
||||||
const { documentId: docId, partitionKeys } = document;
|
|
||||||
test.describe(`Document ID: ${docId}`, () => {
|
|
||||||
test(`should load and view document ${docId}`, async () => {
|
|
||||||
const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0);
|
|
||||||
await span.waitFor();
|
|
||||||
await expect(span).toBeVisible();
|
|
||||||
|
|
||||||
await span.click();
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
|
|
||||||
const resultText = await documentsTab.resultsEditor.text();
|
|
||||||
const resultData = JSON.parse(resultText!);
|
|
||||||
expect(resultText).not.toBeNull();
|
|
||||||
expect(resultData?.id).toEqual(docId);
|
|
||||||
});
|
|
||||||
test(`should be able to create and delete new document from ${docId}`, async ({ page }) => {
|
|
||||||
const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0);
|
|
||||||
await span.waitFor();
|
|
||||||
await expect(span).toBeVisible();
|
|
||||||
|
|
||||||
await span.click();
|
|
||||||
let newDocumentId;
|
|
||||||
await page.waitForTimeout(5000);
|
|
||||||
await retry(async () => {
|
|
||||||
// const discardButton = await explorer.waitForCommandBarButton("Discard", 5000);
|
|
||||||
// if (await discardButton.isEnabled()) {
|
|
||||||
// await discardButton.click();
|
|
||||||
// }
|
|
||||||
const newDocumentButton = await explorer.waitForCommandBarButton("New Item", 5000);
|
|
||||||
await expect(newDocumentButton).toBeVisible();
|
|
||||||
await expect(newDocumentButton).toBeEnabled();
|
|
||||||
await newDocumentButton.click();
|
|
||||||
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
|
|
||||||
newDocumentId = `${Date.now().toString()}-delete`;
|
|
||||||
|
|
||||||
const newDocument = {
|
|
||||||
id: newDocumentId,
|
|
||||||
...setPartitionKeys(partitionKeys || []),
|
|
||||||
};
|
|
||||||
|
|
||||||
await documentsTab.resultsEditor.setText(JSON.stringify(newDocument));
|
|
||||||
const saveButton = await explorer.waitForCommandBarButton("Save", 5000);
|
|
||||||
await saveButton.click({ timeout: 5000 });
|
|
||||||
}, 3);
|
|
||||||
|
|
||||||
const newSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0);
|
|
||||||
await newSpan.waitFor();
|
|
||||||
await newSpan.click();
|
|
||||||
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
|
|
||||||
|
|
||||||
const deleteButton = await explorer.waitForCommandBarButton("Delete", 5000);
|
|
||||||
await deleteButton.click();
|
|
||||||
|
|
||||||
const deleteDialogButton = await explorer.waitForDialogButton("Delete", 5000);
|
|
||||||
await deleteDialogButton.click();
|
|
||||||
|
|
||||||
const deletedSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0);
|
|
||||||
await expect(deletedSpan).toHaveCount(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import { DocumentTestCase } from "../testData";
|
|
||||||
|
|
||||||
export const documentTestCases: DocumentTestCase[] = [
|
|
||||||
{
|
|
||||||
name: "System Partition Key",
|
|
||||||
databaseId: "e2etests-sql-readonly",
|
|
||||||
containerId: "systemPartitionKey",
|
|
||||||
documents: [{ documentId: "systempartition", partitionKeys: [] }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Single Partition Key",
|
|
||||||
databaseId: "e2etests-sql-readonly",
|
|
||||||
containerId: "singlePartitionKey",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey",
|
|
||||||
partitionKeys: [{ key: "/singlePartitionKey", value: "singlePartitionKey" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey_empty_string",
|
|
||||||
partitionKeys: [{ key: "/singlePartitionKey", value: "" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey_null",
|
|
||||||
partitionKeys: [{ key: "/singlePartitionKey", value: null }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey_missing",
|
|
||||||
partitionKeys: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Single Nested Partition Key",
|
|
||||||
databaseId: "e2etests-sql-readonly",
|
|
||||||
containerId: "singleNestedPartitionKey",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey_nested",
|
|
||||||
partitionKeys: [{ key: "/singlePartitionKey/nested", value: "nestedValue" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey_nested_empty_string",
|
|
||||||
partitionKeys: [{ key: "/singlePartitionKey/nested", value: "" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey_nested_null",
|
|
||||||
partitionKeys: [{ key: "/singlePartitionKey/nested", value: null }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "singlePartitionKey_nested_missing",
|
|
||||||
partitionKeys: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "2-Level Hierarchical Partition Key",
|
|
||||||
databaseId: "e2etests-sql-readonly",
|
|
||||||
containerId: "twoLevelPartitionKey",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "twoLevelPartitionKey_value_empty",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/twoLevelPartitionKey_1", value: "value" },
|
|
||||||
{ key: "/twoLevelPartitionKey_2", value: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "twoLevelPartitionKey_value_null",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/twoLevelPartitionKey_1", value: "value" },
|
|
||||||
{ key: "/twoLevelPartitionKey_2", value: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "twoLevelPartitionKey_value_missing",
|
|
||||||
partitionKeys: [{ key: "/twoLevelPartitionKey_1", value: "value" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "twoLevelPartitionKey_empty_null",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/twoLevelPartitionKey_1", value: "" },
|
|
||||||
{ key: "/twoLevelPartitionKey_2", value: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "twoLevelPartitionKey_null_missing",
|
|
||||||
partitionKeys: [{ key: "/twoLevelPartitionKey_1", value: null }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "twoLevelPartitionKey_missing_value",
|
|
||||||
partitionKeys: [{ key: "/twoLevelPartitionKey_2", value: "value" }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "2-Level Hierarchical Nested Partition Key",
|
|
||||||
databaseId: "e2etests-sql-readonly",
|
|
||||||
containerId: "twoLevelNestedPartitionKey",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "twoLevelNestedPartitionKey_nested_value_empty",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/twoLevelNestedPartitionKey/nested", value: "value" },
|
|
||||||
{ key: "/twoLevelNestedPartitionKey/nested_value/nested", value: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "twoLevelNestedPartitionKey_nested_null_missing",
|
|
||||||
partitionKeys: [{ key: "/twoLevelNestedPartitionKey/nested", value: null }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "twoLevelNestedPartitionKey_nested_missing_value",
|
|
||||||
partitionKeys: [{ key: "/twoLevelNestedPartitionKey/nested_value/nested", value: "value" }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "3-Level Hierarchical Partition Key",
|
|
||||||
databaseId: "e2etests-sql-readonly",
|
|
||||||
containerId: "threeLevelPartitionKey",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "threeLevelPartitionKey_value_empty_null",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_2", value: "" },
|
|
||||||
{ key: "/threeLevelPartitionKey_3", value: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelPartitionKey_value_null_missing",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_2", value: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelPartitionKey_value_missing_null",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_3", value: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelPartitionKey_null_empty_value",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1", value: null },
|
|
||||||
{ key: "/threeLevelPartitionKey_2", value: "" },
|
|
||||||
{ key: "/threeLevelPartitionKey_3", value: "value" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelPartitionKey_missing_value_value",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_2", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_3", value: "value" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelPartitionKey_empty_value_missing",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1", value: "" },
|
|
||||||
{ key: "/threeLevelPartitionKey_2", value: "value" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "3-Level Nested Hierarchical Partition Key",
|
|
||||||
databaseId: "e2etests-sql-readonly",
|
|
||||||
containerId: "threeLevelNestedPartitionKey",
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_empty_value_null",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_key", value: "" },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_key", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_3/nested_key", value: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_null_value_missing",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_key", value: null },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_key", value: "value" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_missing_value_null",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_key", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_3/nested_key", value: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_null_empty_missing",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_key", value: null },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_key", value: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_value_missing_empty",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_key", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_3/nested_key", value: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_missing_null_empty",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_key", value: null },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_3/nested_key", value: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_empty_null_value",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_key", value: "" },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_key", value: null },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_3/nested_key", value: "value" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentId: "threeLevelNestedPartitionKey_nested_value_null_empty",
|
|
||||||
partitionKeys: [
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_key", value: "value" },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_key", value: null },
|
|
||||||
{ key: "/threeLevelPartitionKey_1/nested/nested_2/nested_3/nested_key", value: "" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -16,23 +16,6 @@ export interface TestItem {
|
|||||||
randomData: string;
|
randomData: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTestCase {
|
|
||||||
name: string;
|
|
||||||
databaseId: string;
|
|
||||||
containerId: string;
|
|
||||||
documents: TestDocument[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestDocument {
|
|
||||||
documentId: string;
|
|
||||||
partitionKeys?: PartitionKey[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PartitionKey {
|
|
||||||
key: string;
|
|
||||||
value: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const partitionCount = 4;
|
const partitionCount = 4;
|
||||||
|
|
||||||
// If we increase this number, we need to split bulk creates into multiple batches.
|
// If we increase this number, we need to split bulk creates into multiple batches.
|
||||||
@@ -110,46 +93,3 @@ export async function createTestSQLContainer(includeTestData?: boolean) {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setPartitionKeys = (partitionKeys: PartitionKey[]) => {
|
|
||||||
const result = {};
|
|
||||||
|
|
||||||
partitionKeys.forEach((partitionKey) => {
|
|
||||||
const { key: keyPath, value: keyValue } = partitionKey;
|
|
||||||
const cleanPath = keyPath.startsWith("/") ? keyPath.slice(1) : keyPath;
|
|
||||||
const keys = cleanPath.split("/");
|
|
||||||
let current = result;
|
|
||||||
|
|
||||||
keys.forEach((key, index) => {
|
|
||||||
if (index === keys.length - 1) {
|
|
||||||
current[key] = keyValue;
|
|
||||||
} else {
|
|
||||||
current[key] = current[key] || {};
|
|
||||||
current = current[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializeMongoToJson = (text: string) => {
|
|
||||||
const normalized = text.replace(/ObjectId\("([0-9a-fA-F]{24})"\)/g, '"$1"');
|
|
||||||
return JSON.parse(normalized);
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function retry<T>(fn: () => Promise<T>, retries = 3, delayMs = 1000): Promise<T> {
|
|
||||||
let lastError: unknown;
|
|
||||||
for (let i = 0; i < retries; i++) {
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error;
|
|
||||||
console.warn(`Retry ${i + 1}/${retries} failed: ${(error as Error).message}`);
|
|
||||||
if (i < retries - 1) {
|
|
||||||
await new Promise((res) => setTimeout(res, delayMs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastError;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ async function main() {
|
|||||||
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||||
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
if (account.name.endsWith("-readonly")) {
|
|
||||||
console.log(`SKIPPED: ${account.name}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (account.kind === "MongoDB") {
|
if (account.kind === "MongoDB") {
|
||||||
const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name);
|
const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name);
|
||||||
for (const database of mongoDatabases) {
|
for (const database of mongoDatabases) {
|
||||||
|
|||||||
Reference in New Issue
Block a user