mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-25 11:51:07 +00:00
Compare commits
18 Commits
release/ma
...
missing_pk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e8db9b539 | ||
|
|
f94a452a98 | ||
|
|
6ce81099ef | ||
|
|
777e411f4f | ||
|
|
63d4b4f4ef | ||
|
|
eaf9a14e7d | ||
|
|
5997fabcda | ||
|
|
f01d4a5ae2 | ||
|
|
20eeed98e4 | ||
|
|
ac53e1b3b5 | ||
|
|
2cab086268 | ||
|
|
937451d844 | ||
|
|
5dfaa9f0f8 | ||
|
|
05e2d0ac29 | ||
|
|
152c995ec0 | ||
|
|
07c4ca9c50 | ||
|
|
80781f7c8f | ||
|
|
aa39359460 |
@@ -1914,13 +1914,20 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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": "4.6.2",
|
"p-retry": "6.2.1",
|
||||||
"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,7 +12662,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/retry": {
|
"node_modules/@types/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||||
|
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/sanitize-html": {
|
"node_modules/@types/sanitize-html": {
|
||||||
@@ -21799,6 +21801,18 @@
|
|||||||
"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",
|
||||||
@@ -30243,14 +30257,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-retry": {
|
"node_modules/p-retry": {
|
||||||
"version": "4.6.2",
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/retry": "0.12.0",
|
"@types/retry": "0.12.2",
|
||||||
|
"is-network-error": "^1.0.0",
|
||||||
"retry": "^0.13.1"
|
"retry": "^0.13.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=16.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-try": {
|
"node_modules/p-try": {
|
||||||
@@ -35997,6 +36017,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webpack-dev-server/node_modules/@types/retry": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/webpack-dev-server/node_modules/ajv": {
|
"node_modules/webpack-dev-server/node_modules/ajv": {
|
||||||
"version": "8.12.0",
|
"version": "8.12.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -36044,6 +36071,20 @@
|
|||||||
"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": "4.6.2",
|
"p-retry": "6.2.1",
|
||||||
"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",
|
||||||
|
|||||||
37913
preview/package-lock.json
generated
37913
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -530,9 +530,6 @@ 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 MaterializedViewsLabels {
|
|
||||||
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,3 @@ export function getWorkloadType(): WorkloadType {
|
|||||||
}
|
}
|
||||||
return workloadType;
|
return workloadType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMaterializedViewsEnabled(): boolean {
|
|
||||||
return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import { constructRpOptions } from "Common/dataAccess/createCollection";
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
|
||||||
import { Collection, CreateMaterializedViewsParams } from "Contracts/DataModels";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
|
|
||||||
import {
|
|
||||||
CreateUpdateOptions,
|
|
||||||
SqlContainerResource,
|
|
||||||
SqlDatabaseCreateUpdateParameters,
|
|
||||||
} from "Utils/arm/generatedClients/cosmos/types";
|
|
||||||
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
|
||||||
|
|
||||||
export const createMaterializedView = async (params: CreateMaterializedViewsParams): Promise<Collection> => {
|
|
||||||
const clearMessage = logConsoleProgress(
|
|
||||||
`Creating a new materialized view ${params.materializedViewId} for database ${params.databaseId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
|
||||||
|
|
||||||
const resource: SqlContainerResource = {
|
|
||||||
id: params.materializedViewId,
|
|
||||||
};
|
|
||||||
if (params.materializedViewDefinition) {
|
|
||||||
resource.materializedViewDefinition = params.materializedViewDefinition;
|
|
||||||
}
|
|
||||||
if (params.analyticalStorageTtl) {
|
|
||||||
resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
|
||||||
}
|
|
||||||
if (params.indexingPolicy) {
|
|
||||||
resource.indexingPolicy = params.indexingPolicy;
|
|
||||||
}
|
|
||||||
if (params.partitionKey) {
|
|
||||||
resource.partitionKey = params.partitionKey;
|
|
||||||
}
|
|
||||||
if (params.uniqueKeyPolicy) {
|
|
||||||
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
|
||||||
}
|
|
||||||
if (params.vectorEmbeddingPolicy) {
|
|
||||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
|
||||||
}
|
|
||||||
if (params.fullTextPolicy) {
|
|
||||||
resource.fullTextPolicy = params.fullTextPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
|
||||||
properties: {
|
|
||||||
resource,
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const createResponse = await createUpdateSqlContainer(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
params.databaseId,
|
|
||||||
params.materializedViewId,
|
|
||||||
rpPayload,
|
|
||||||
);
|
|
||||||
logConsoleInfo(`Successfully created materialized view ${params.materializedViewId}`);
|
|
||||||
|
|
||||||
return createResponse && (createResponse.properties.resource as Collection);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "CreateMaterializedView", `Error while creating materialized view ${params.materializedViewId}`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -126,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
|||||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TO DO: Remove when we get RP API Spec with materializedViews
|
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
return rpResponse?.value?.map((collection: any) => {
|
|
||||||
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
|
|
||||||
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
|
|
||||||
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
|
|
||||||
return collectionDataModel;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
writeLocations?: DatabaseAccountResponseLocation[];
|
writeLocations?: DatabaseAccountResponseLocation[];
|
||||||
enableFreeTier?: boolean;
|
enableFreeTier?: boolean;
|
||||||
enableAnalyticalStorage?: boolean;
|
enableAnalyticalStorage?: boolean;
|
||||||
enableMaterializedViews?: boolean;
|
|
||||||
isVirtualNetworkFilterEnabled?: boolean;
|
isVirtualNetworkFilterEnabled?: boolean;
|
||||||
ipRules?: IpRule[];
|
ipRules?: IpRule[];
|
||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
@@ -165,8 +164,6 @@ export interface Collection extends Resource {
|
|||||||
schema?: ISchema;
|
schema?: ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
computedProperties?: ComputedProperties;
|
computedProperties?: ComputedProperties;
|
||||||
materializedViews?: MaterializedView[];
|
|
||||||
materializedViewDefinition?: MaterializedViewDefinition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionsWithPagination {
|
export interface CollectionsWithPagination {
|
||||||
@@ -226,17 +223,6 @@ export interface ComputedProperty {
|
|||||||
|
|
||||||
export type ComputedProperties = ComputedProperty[];
|
export type ComputedProperties = ComputedProperty[];
|
||||||
|
|
||||||
export interface MaterializedView {
|
|
||||||
id: string;
|
|
||||||
_rid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MaterializedViewDefinition {
|
|
||||||
definition: string;
|
|
||||||
sourceCollectionId: string;
|
|
||||||
sourceCollectionRid?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PartitionKey {
|
export interface PartitionKey {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
kind: "Hash" | "Range" | "MultiHash";
|
kind: "Hash" | "Range" | "MultiHash";
|
||||||
@@ -359,7 +345,9 @@ export interface CreateDatabaseParams {
|
|||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCollectionParamsBase {
|
export interface CreateCollectionParams {
|
||||||
|
createNewDatabase: boolean;
|
||||||
|
collectionId: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
@@ -373,16 +361,6 @@ export interface CreateCollectionParamsBase {
|
|||||||
fullTextPolicy?: FullTextPolicy;
|
fullTextPolicy?: FullTextPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
|
||||||
createNewDatabase: boolean;
|
|
||||||
collectionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
|
||||||
materializedViewId: string;
|
|
||||||
materializedViewDefinition: MaterializedViewDefinition;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VectorEmbeddingPolicy {
|
export interface VectorEmbeddingPolicy {
|
||||||
vectorEmbeddings: VectorEmbedding[];
|
vectorEmbeddings: VectorEmbedding[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,8 +143,6 @@ export interface Collection extends CollectionBase {
|
|||||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
documentIds: ko.ObservableArray<DocumentId>;
|
documentIds: ko.ObservableArray<DocumentId>;
|
||||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||||
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
|
|
||||||
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
|
|
||||||
|
|
||||||
cassandraKeys: CassandraTableKeys;
|
cassandraKeys: CassandraTableKeys;
|
||||||
cassandraSchema: CassandraTableKey[];
|
cassandraSchema: CassandraTableKey[];
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { MaterializedViewsLabels } from "Common/Constants";
|
|
||||||
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 {
|
|
||||||
AddMaterializedViewPanel,
|
|
||||||
AddMaterializedViewPanelProps,
|
|
||||||
} 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,24 +164,6 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMaterializedViewsEnabled() && !selectedCollection.materializedViewDefinition()) {
|
|
||||||
items.push({
|
|
||||||
label: MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
onClick: () => {
|
|
||||||
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
|
||||||
explorer: container,
|
|
||||||
sourceContainer: selectedCollection,
|
|
||||||
};
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ import {
|
|||||||
ConflictResolutionComponentProps,
|
ConflictResolutionComponentProps,
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||||
import {
|
|
||||||
MaterializedViewComponent,
|
|
||||||
MaterializedViewComponentProps,
|
|
||||||
} from "./SettingsSubComponents/MaterializedViewComponent";
|
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps,
|
MongoIndexingPolicyComponentProps,
|
||||||
@@ -166,7 +162,6 @@ 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 isMaterializedView: boolean;
|
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
private isFullTextSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
@@ -184,8 +179,6 @@ 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.isMaterializedView =
|
|
||||||
!!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);
|
||||||
|
|
||||||
@@ -1279,11 +1272,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
|
||||||
collection: this.collection,
|
|
||||||
explorer: this.props.settingsTab.getContainer(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -1347,13 +1335,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMaterializedView) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.MaterializedViewTab,
|
|
||||||
content: <MaterializedViewComponent {...materializedViewComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import React from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
|
||||||
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
|
||||||
|
|
||||||
export interface MaterializedViewComponentProps {
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MaterializedViewComponent: React.FC<MaterializedViewComponentProps> = ({ collection, explorer }) => {
|
|
||||||
const isTargetContainer = !!collection?.materializedViewDefinition();
|
|
||||||
const isSourceContainer = !!collection?.materializedViews();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
|
||||||
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
|
||||||
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following views defined for it.</Text>
|
|
||||||
<Text>
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
|
||||||
</Link>{" "}
|
|
||||||
about how to define materialized views and how to use them.
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
{isSourceContainer && <MaterializedViewSourceComponent collection={collection} explorer={explorer} />}
|
|
||||||
{isTargetContainer && <MaterializedViewTargetComponent collection={collection} />}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { PrimaryButton } from "@fluentui/react";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { collection, container } from "../TestUtils";
|
|
||||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
|
||||||
|
|
||||||
describe("MaterializedViewSourceComponent", () => {
|
|
||||||
let testCollection: typeof collection;
|
|
||||||
let testExplorer: typeof container;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testCollection = { ...collection };
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the PrimaryButton", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates when new materialized views are provided", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
|
||||||
|
|
||||||
// Simulating an update by modifying the observable directly
|
|
||||||
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
|
|
||||||
|
|
||||||
wrapper.setProps({ collection: testCollection });
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { PrimaryButton } from "@fluentui/react";
|
|
||||||
import { MaterializedViewsLabels } from "Common/Constants";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
|
||||||
import { AddMaterializedViewPanel } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import * as monaco from "monaco-editor";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
export interface MaterializedViewSourceComponentProps {
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MaterializedViewSourceComponent: React.FC<MaterializedViewSourceComponentProps> = ({
|
|
||||||
collection,
|
|
||||||
explorer,
|
|
||||||
}) => {
|
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
|
||||||
|
|
||||||
const materializedViews = collection?.materializedViews() ?? [];
|
|
||||||
|
|
||||||
// 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[] } => {
|
|
||||||
let definition = "";
|
|
||||||
let partitionKey: string[] = [];
|
|
||||||
|
|
||||||
useDatabases.getState().databases.find((database) => {
|
|
||||||
const collection = database.collections().find((collection) => collection.id() === viewId);
|
|
||||||
if (collection) {
|
|
||||||
const materializedViewDefinition = collection.materializedViewDefinition();
|
|
||||||
materializedViewDefinition && (definition = materializedViewDefinition.definition);
|
|
||||||
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { definition, partitionKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
//JSON value for the editor using the fetched id and definitions.
|
|
||||||
const jsonValue = JSON.stringify(
|
|
||||||
materializedViews.map((view) => {
|
|
||||||
const { definition, partitionKey } = getViewDetails(view.id);
|
|
||||||
return {
|
|
||||||
name: view.id,
|
|
||||||
partitionKey: partitionKey.join(", "),
|
|
||||||
definition,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize Monaco editor with the computed JSON value.
|
|
||||||
useEffect(() => {
|
|
||||||
let disposed = false;
|
|
||||||
const initMonaco = async () => {
|
|
||||||
const monacoInstance = await loadMonaco();
|
|
||||||
if (disposed || !editorContainerRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
|
||||||
value: jsonValue,
|
|
||||||
language: "json",
|
|
||||||
ariaLabel: "Materialized Views JSON",
|
|
||||||
readOnly: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
initMonaco();
|
|
||||||
return () => {
|
|
||||||
disposed = true;
|
|
||||||
editorRef.current?.dispose();
|
|
||||||
};
|
|
||||||
}, [jsonValue]);
|
|
||||||
|
|
||||||
// Update the editor when the jsonValue changes.
|
|
||||||
useEffect(() => {
|
|
||||||
if (editorRef.current) {
|
|
||||||
editorRef.current.setValue(jsonValue);
|
|
||||||
}
|
|
||||||
}, [jsonValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
ref={editorContainerRef}
|
|
||||||
style={{
|
|
||||||
height: 250,
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
borderRadius: 4,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PrimaryButton
|
|
||||||
text="Add view"
|
|
||||||
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
|
||||||
onClick={() =>
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
<AddMaterializedViewPanel explorer={explorer} sourceContainer={collection} />,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Text } from "@fluentui/react";
|
|
||||||
import { Collection } from "Contracts/ViewModels";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { collection } from "../TestUtils";
|
|
||||||
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
|
||||||
|
|
||||||
describe("MaterializedViewTargetComponent", () => {
|
|
||||||
let testCollection: Collection;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testCollection = {
|
|
||||||
...collection,
|
|
||||||
materializedViewDefinition: collection.materializedViewDefinition,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays the source container ID", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
|
||||||
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays the materialized view definition", () => {
|
|
||||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
|
||||||
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { Stack, Text } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
export interface MaterializedViewTargetComponentProps {
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MaterializedViewTargetComponent: React.FC<MaterializedViewTargetComponentProps> = ({ collection }) => {
|
|
||||||
const materializedViewDefinition = collection?.materializedViewDefinition();
|
|
||||||
|
|
||||||
const textHeadingStyle = {
|
|
||||||
root: { fontWeight: "600", fontSize: 16 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const valueBoxStyle = {
|
|
||||||
root: {
|
|
||||||
backgroundColor: "#f3f3f3",
|
|
||||||
padding: "5px 10px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
|
||||||
<Text styles={textHeadingStyle}>Materialized View Settings</Text>
|
|
||||||
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
|
||||||
<Stack styles={valueBoxStyle}>
|
|
||||||
<Text>{materializedViewDefinition?.sourceCollectionId}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text styles={{ root: { fontWeight: "600" } }}>Materialized view definition</Text>
|
|
||||||
<Stack styles={valueBoxStyle}>
|
|
||||||
<Text>{materializedViewDefinition?.definition}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -57,7 +57,6 @@ export enum SettingsV2TabTypes {
|
|||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
ThroughputBucketsTab,
|
||||||
MaterializedViewTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
export enum ContainerPolicyTabTypes {
|
||||||
@@ -172,8 +171,6 @@ 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.MaterializedViewTab:
|
|
||||||
return "Materialized Views (Preview)";
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,15 +48,6 @@ export const collection = {
|
|||||||
]),
|
]),
|
||||||
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
||||||
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
||||||
materializedViews: ko.observable<DataModels.MaterializedView[]>([
|
|
||||||
{ id: "view1", _rid: "rid1" },
|
|
||||||
{ id: "view2", _rid: "rid2" },
|
|
||||||
]),
|
|
||||||
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
|
|
||||||
definition: "SELECT * FROM c WHERE c.id = 1",
|
|
||||||
sourceCollectionId: "source1",
|
|
||||||
sourceCollectionRid: "rid123",
|
|
||||||
}),
|
|
||||||
readSettings: () => {
|
readSettings: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -141,8 +139,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -262,8 +258,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"getDatabase": [Function],
|
"getDatabase": [Function],
|
||||||
"id": [Function],
|
"id": [Function],
|
||||||
"indexingPolicy": [Function],
|
"indexingPolicy": [Function],
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
"offer": [Function],
|
||||||
"partitionKey": {
|
"partitionKey": {
|
||||||
"kind": "hash",
|
"kind": "hash",
|
||||||
@@ -342,101 +336,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
shouldDiscardComputedProperties={false}
|
shouldDiscardComputedProperties={false}
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
|
||||||
headerText="Materialized Views (Preview)"
|
|
||||||
itemKey="MaterializedViewTab"
|
|
||||||
key="MaterializedViewTab"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"marginTop": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MaterializedViewComponent
|
|
||||||
collection={
|
|
||||||
{
|
|
||||||
"analyticalStorageTtl": [Function],
|
|
||||||
"changeFeedPolicy": [Function],
|
|
||||||
"computedProperties": [Function],
|
|
||||||
"conflictResolutionPolicy": [Function],
|
|
||||||
"container": Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"databaseId": "test",
|
|
||||||
"defaultTtl": [Function],
|
|
||||||
"fullTextPolicy": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
|
||||||
"getDatabase": [Function],
|
|
||||||
"id": [Function],
|
|
||||||
"indexingPolicy": [Function],
|
|
||||||
"materializedViewDefinition": [Function],
|
|
||||||
"materializedViews": [Function],
|
|
||||||
"offer": [Function],
|
|
||||||
"partitionKey": {
|
|
||||||
"kind": "hash",
|
|
||||||
"paths": [],
|
|
||||||
"version": 2,
|
|
||||||
},
|
|
||||||
"partitionKeyProperties": [
|
|
||||||
"partitionKey",
|
|
||||||
],
|
|
||||||
"readSettings": [Function],
|
|
||||||
"uniqueKeyPolicy": {},
|
|
||||||
"usageSizeInKB": [Function],
|
|
||||||
"vectorEmbeddingPolicy": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PivotItem>
|
|
||||||
</StyledPivot>
|
</StyledPivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
|
|||||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||||
|
|||||||
@@ -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 } from "p-retry";
|
import promiseRetry, { AbortError, Options } 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: promiseRetry.Options;
|
private retryOptions: Options;
|
||||||
private scheduleTimerId: NodeJS.Timeout;
|
private scheduleTimerId: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(private onConnectionLost: () => void) {
|
constructor(private onConnectionLost: () => void) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { AddCollectionPanel } from "./AddCollectionPanel";
|
import { AddCollectionPanel } from "./AddCollectionPanel";
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -21,25 +21,11 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
|||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
|
||||||
import {
|
import {
|
||||||
AllPropertiesIndexed,
|
FullTextPoliciesComponent,
|
||||||
AnalyticalStorageContent,
|
getFullTextLanguageOptions,
|
||||||
ContainerVectorPolicyTooltipContent,
|
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||||
FullTextPolicyDefault,
|
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||||
getPartitionKey,
|
|
||||||
getPartitionKeyName,
|
|
||||||
getPartitionKeyPlaceHolder,
|
|
||||||
getPartitionKeyTooltipText,
|
|
||||||
isFreeTierAccount,
|
|
||||||
isSynapseLinkEnabled,
|
|
||||||
parseUniqueKeys,
|
|
||||||
scrollToSection,
|
|
||||||
SharedDatabaseDefault,
|
|
||||||
shouldShowAnalyticalStoreOptions,
|
|
||||||
UniqueKeysHeader,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
@@ -56,15 +42,15 @@ import {
|
|||||||
isVectorSearchEnabled,
|
isVectorSearchEnabled,
|
||||||
} from "Utils/CapabilityUtils";
|
} from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
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 "../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";
|
||||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
|
||||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
||||||
|
|
||||||
export interface AddCollectionPanelProps {
|
export interface AddCollectionPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -72,6 +58,40 @@ export interface AddCollectionPanelProps {
|
|||||||
isQuickstart?: boolean;
|
isQuickstart?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||||
|
indexingMode: "consistent",
|
||||||
|
automatic: true,
|
||||||
|
includedPaths: [],
|
||||||
|
excludedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||||
|
indexingMode: "consistent",
|
||||||
|
automatic: true,
|
||||||
|
includedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
kind: "Range",
|
||||||
|
dataType: "Number",
|
||||||
|
precision: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "Range",
|
||||||
|
dataType: "String",
|
||||||
|
precision: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
excludedPaths: [],
|
||||||
|
};
|
||||||
|
|
||||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||||
vectorEmbeddings: [],
|
vectorEmbeddings: [],
|
||||||
};
|
};
|
||||||
@@ -124,7 +144,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||||
enableIndexing: true,
|
enableIndexing: true,
|
||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: getPartitionKey(props.isQuickstart),
|
partitionKey: this.getPartitionKey(),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
@@ -140,7 +160,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
vectorEmbeddingPolicy: [],
|
vectorEmbeddingPolicy: [],
|
||||||
vectorIndexingPolicy: [],
|
vectorIndexingPolicy: [],
|
||||||
vectorPolicyValidated: true,
|
vectorPolicyValidated: true,
|
||||||
fullTextPolicy: FullTextPolicyDefault,
|
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
|
||||||
fullTextIndexes: [],
|
fullTextIndexes: [],
|
||||||
fullTextPolicyValidated: true,
|
fullTextPolicyValidated: true,
|
||||||
};
|
};
|
||||||
@@ -154,7 +174,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
||||||
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
||||||
scrollToSection("panelContainer");
|
this.scrollToSection("panelContainer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +191,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.state.errorMessage && isFreeTierAccount() && (
|
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||||
<PanelInfoErrorComponent
|
<PanelInfoErrorComponent
|
||||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||||
messageType="info"
|
messageType="info"
|
||||||
@@ -379,10 +399,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={true}
|
isDatabase={true}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={this.isFreeTierAccount()}
|
||||||
isQuickstart={this.props.isQuickstart}
|
isQuickstart={this.props.isQuickstart}
|
||||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||||
@@ -559,14 +579,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{getPartitionKeyName()}
|
{this.getPartitionKeyName()}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={this.getPartitionKeyTooltipText()}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ariaLabel={getPartitionKeyTooltipText()}
|
ariaLabel={this.getPartitionKeyTooltipText()}
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -580,8 +603,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
placeholder={getPartitionKeyPlaceHolder()}
|
placeholder={this.getPartitionKeyPlaceHolder()}
|
||||||
aria-label={getPartitionKeyName()}
|
aria-label={this.getPartitionKeyName()}
|
||||||
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
||||||
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
||||||
value={this.state.partitionKey}
|
value={this.state.partitionKey}
|
||||||
@@ -619,8 +642,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
tabIndex={index > 0 ? 1 : 0}
|
tabIndex={index > 0 ? 1 : 0}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={getPartitionKeyPlaceHolder(index)}
|
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
||||||
aria-label={getPartitionKeyName()}
|
aria-label={this.getPartitionKeyName()}
|
||||||
pattern={".*"}
|
pattern={".*"}
|
||||||
title={""}
|
title={""}
|
||||||
value={subPartitionKey}
|
value={subPartitionKey}
|
||||||
@@ -711,10 +734,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={this.isFreeTierAccount()}
|
||||||
isQuickstart={this.props.isQuickstart}
|
isQuickstart={this.props.isQuickstart}
|
||||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||||
@@ -729,7 +752,27 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||||
<Stack>
|
<Stack>
|
||||||
{UniqueKeysHeader()}
|
<Stack horizontal>
|
||||||
|
<Text className="panelTextBold" variant="small">
|
||||||
|
Unique keys
|
||||||
|
</Text>
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={
|
||||||
|
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={
|
||||||
|
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
||||||
@@ -777,10 +820,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
{this.shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{AnalyticalStorageContent()}
|
{this.getAnalyticalStorageContent()}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
@@ -788,7 +831,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={this.state.enableAnalyticalStore}
|
checked={this.state.enableAnalyticalStore}
|
||||||
disabled={!isSynapseLinkEnabled()}
|
disabled={!this.isSynapseLinkEnabled()}
|
||||||
aria-label="Enable analytical store"
|
aria-label="Enable analytical store"
|
||||||
aria-checked={this.state.enableAnalyticalStore}
|
aria-checked={this.state.enableAnalyticalStore}
|
||||||
name="analyticalStore"
|
name="analyticalStore"
|
||||||
@@ -803,7 +846,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={!this.state.enableAnalyticalStore}
|
checked={!this.state.enableAnalyticalStore}
|
||||||
disabled={!isSynapseLinkEnabled()}
|
disabled={!this.isSynapseLinkEnabled()}
|
||||||
aria-label="Disable analytical store"
|
aria-label="Disable analytical store"
|
||||||
aria-checked={!this.state.enableAnalyticalStore}
|
aria-checked={!this.state.enableAnalyticalStore}
|
||||||
name="analyticalStore"
|
name="analyticalStore"
|
||||||
@@ -817,7 +860,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
{!this.isSynapseLinkEnabled() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Azure Synapse Link is required for creating an analytical store{" "}
|
Azure Synapse Link is required for creating an analytical store{" "}
|
||||||
@@ -847,9 +890,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
title="Container Vector Policy"
|
title="Container Vector Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
@@ -875,7 +918,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
title="Container Full Text Search Policy"
|
title="Container Full Text Search Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
this.scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
//TODO: uncomment when learn more text becomes available
|
//TODO: uncomment when learn more text becomes available
|
||||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||||
@@ -903,7 +946,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||||
scrollToSection("collapsibleAdvancedSectionContent");
|
this.scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||||
@@ -1013,6 +1056,31 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPartitionKeyName(isLowerCase?: boolean): string {
|
||||||
|
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||||
|
|
||||||
|
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPartitionKeyPlaceHolder(index?: number): string {
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "Mongo":
|
||||||
|
return "e.g., categoryId";
|
||||||
|
case "Gremlin":
|
||||||
|
return "e.g., /address";
|
||||||
|
case "SQL":
|
||||||
|
return `${
|
||||||
|
index === undefined
|
||||||
|
? "Required - first partition key e.g., /TenantId"
|
||||||
|
: index === 0
|
||||||
|
? "second partition key e.g., /UserId"
|
||||||
|
: "third partition key e.g., /SessionId"
|
||||||
|
}`;
|
||||||
|
default:
|
||||||
|
return "e.g., /address/zipCode";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
if (event.target.checked && !this.state.createNewDatabase) {
|
if (event.target.checked && !this.state.createNewDatabase) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -1100,12 +1168,48 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return !!selectedDatabase?.offer();
|
return !!selectedDatabase?.offer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isFreeTierAccount(): boolean {
|
||||||
|
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
|
}
|
||||||
|
|
||||||
private getFreeTierIndexingText(): string {
|
private getFreeTierIndexingText(): string {
|
||||||
return this.state.enableIndexing
|
return this.state.enableIndexing
|
||||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||||
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPartitionKeyTooltipText(): string {
|
||||||
|
if (userContext.apiType === "Mongo") {
|
||||||
|
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||||
|
}
|
||||||
|
|
||||||
|
let tooltipText = `The ${this.getPartitionKeyName(
|
||||||
|
true,
|
||||||
|
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||||
|
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPartitionKey(): string {
|
||||||
|
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (userContext.features.partitionKeyDefault) {
|
||||||
|
return userContext.apiType === "SQL" ? "/id" : "_id";
|
||||||
|
}
|
||||||
|
if (userContext.features.partitionKeyDefault2) {
|
||||||
|
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||||
|
}
|
||||||
|
if (this.props.isQuickstart) {
|
||||||
|
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private getPartitionKeySubtext(): string {
|
private getPartitionKeySubtext(): string {
|
||||||
if (
|
if (
|
||||||
userContext.features.partitionKeyDefault &&
|
userContext.features.partitionKeyDefault &&
|
||||||
@@ -1117,6 +1221,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAnalyticalStorageContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text variant="small">
|
||||||
|
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||||
|
impacting the performance of transactional workloads.{" "}
|
||||||
|
<Link
|
||||||
|
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
||||||
|
target="_blank"
|
||||||
|
href="https://aka.ms/analytical-store-overview"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text variant="small">
|
||||||
|
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||||
|
queries.{" "}
|
||||||
|
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: uncomment when learn more text becomes available
|
//TODO: uncomment when learn more text becomes available
|
||||||
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
||||||
// return (
|
// return (
|
||||||
@@ -1147,7 +1279,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||||
if (!isFreeTierAccount()) {
|
if (!this.isFreeTierAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,6 +1288,39 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
: this.isSelectedDatabaseSharedThroughput();
|
: this.isSelectedDatabaseSharedThroughput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldShowAnalyticalStoreOptions(): boolean {
|
||||||
|
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "SQL":
|
||||||
|
case "Mongo":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSynapseLinkEnabled(): boolean {
|
||||||
|
if (!userContext.databaseAccount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { properties } = userContext.databaseAccount;
|
||||||
|
if (!properties) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.enableAnalyticalStorage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties.capabilities?.some(
|
||||||
|
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private shouldShowVectorSearchParameters() {
|
private shouldShowVectorSearchParameters() {
|
||||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
}
|
}
|
||||||
@@ -1236,11 +1401,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTtl(): number {
|
private getAnalyticalStorageTtl(): number {
|
||||||
if (!isSynapseLinkEnabled()) {
|
if (!this.isSynapseLinkEnabled()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldShowAnalyticalStoreOptions()) {
|
if (!this.shouldShowAnalyticalStoreOptions()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1254,6 +1419,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return Constants.AnalyticalStorageTtl.Disabled;
|
return Constants.AnalyticalStorageTtl.Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private scrollToSection(id: string): void {
|
||||||
|
document.getElementById(id)?.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
private getSampleDBName(): string {
|
private getSampleDBName(): string {
|
||||||
const existingSampleDBs = useDatabases
|
const existingSampleDBs = useDatabases
|
||||||
.getState()
|
.getState()
|
||||||
@@ -1288,7 +1457,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
partitionKeyString = "/'$pk'";
|
partitionKeyString = "/'$pk'";
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import { configContext, Platform } from "ConfigContext";
|
|
||||||
import * as DataModels from "Contracts/DataModels";
|
|
||||||
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import React from "react";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
|
|
||||||
export function getPartitionKeyTooltipText(): string {
|
|
||||||
if (userContext.apiType === "Mongo") {
|
|
||||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
|
||||||
}
|
|
||||||
|
|
||||||
let tooltipText = `The ${getPartitionKeyName(
|
|
||||||
true,
|
|
||||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
|
||||||
|
|
||||||
if (userContext.apiType === "SQL") {
|
|
||||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return tooltipText;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartitionKeyName(isLowerCase?: boolean): string {
|
|
||||||
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
|
||||||
|
|
||||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartitionKeyPlaceHolder(index?: number): string {
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "Mongo":
|
|
||||||
return "e.g., categoryId";
|
|
||||||
case "Gremlin":
|
|
||||||
return "e.g., /address";
|
|
||||||
case "SQL":
|
|
||||||
return `${
|
|
||||||
index === undefined
|
|
||||||
? "Required - first partition key e.g., /TenantId"
|
|
||||||
: index === 0
|
|
||||||
? "second partition key e.g., /UserId"
|
|
||||||
: "third partition key e.g., /SessionId"
|
|
||||||
}`;
|
|
||||||
default:
|
|
||||||
return "e.g., /address/zipCode";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartitionKey(isQuickstart?: boolean): string {
|
|
||||||
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (userContext.features.partitionKeyDefault) {
|
|
||||||
return userContext.apiType === "SQL" ? "/id" : "_id";
|
|
||||||
}
|
|
||||||
if (userContext.features.partitionKeyDefault2) {
|
|
||||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
|
||||||
}
|
|
||||||
if (isQuickstart) {
|
|
||||||
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isFreeTierAccount(): boolean {
|
|
||||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UniqueKeysHeader(): JSX.Element {
|
|
||||||
const tooltipContent =
|
|
||||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack horizontal>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Unique keys
|
|
||||||
</Text>
|
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldShowAnalyticalStoreOptions(): boolean {
|
|
||||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "SQL":
|
|
||||||
case "Mongo":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AnalyticalStorageContent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Text variant="small">
|
|
||||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
|
|
||||||
the performance of transactional workloads.{" "}
|
|
||||||
<Link
|
|
||||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
|
||||||
target="_blank"
|
|
||||||
href="https://aka.ms/analytical-store-overview"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSynapseLinkEnabled(): boolean {
|
|
||||||
if (!userContext.databaseAccount) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { properties } = userContext.databaseAccount;
|
|
||||||
if (!properties) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (properties.enableAnalyticalStorage) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties.capabilities?.some(
|
|
||||||
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollToSection(id: string): void {
|
|
||||||
document.getElementById(id)?.scrollIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Text variant="small">
|
|
||||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
|
||||||
queries.{" "}
|
|
||||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy {
|
|
||||||
if (uniqueKeys?.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] };
|
|
||||||
uniqueKeys.forEach((uniqueKey: string) => {
|
|
||||||
if (uniqueKey) {
|
|
||||||
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
|
|
||||||
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
|
|
||||||
if (trimmedPaths?.length > 0) {
|
|
||||||
if (userContext.apiType === "Mongo") {
|
|
||||||
trimmedPaths.map((path) => {
|
|
||||||
const transformedPath = path.split(".").join("/");
|
|
||||||
if (transformedPath[0] !== "/") {
|
|
||||||
return "/" + transformedPath;
|
|
||||||
}
|
|
||||||
return transformedPath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return uniqueKeyPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
|
||||||
indexingMode: "consistent",
|
|
||||||
automatic: true,
|
|
||||||
includedPaths: [],
|
|
||||||
excludedPaths: [
|
|
||||||
{
|
|
||||||
path: "/*",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FullTextPolicyDefault: DataModels.FullTextPolicy = {
|
|
||||||
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
|
|
||||||
fullTextPaths: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
|
||||||
indexingMode: "consistent",
|
|
||||||
automatic: true,
|
|
||||||
includedPaths: [
|
|
||||||
{
|
|
||||||
path: "/*",
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
kind: "Range",
|
|
||||||
dataType: "Number",
|
|
||||||
precision: -1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "Range",
|
|
||||||
dataType: "String",
|
|
||||||
precision: -1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
excludedPaths: [],
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react";
|
|
||||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
|
||||||
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
|
||||||
|
|
||||||
export interface AddMVAdvancedComponentProps {
|
|
||||||
useHashV1: boolean;
|
|
||||||
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
}
|
|
||||||
export const AddMVAdvancedComponent = (props: AddMVAdvancedComponentProps): JSX.Element => {
|
|
||||||
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
|
|
||||||
|
|
||||||
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
|
|
||||||
setUseHashV1(isChecked);
|
|
||||||
setSubPartitionKeys([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Advanced"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddMaterializedViewPaneAdvancedSection);
|
|
||||||
scrollToSection("collapsibleAdvancedSectionContent");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
|
||||||
<Checkbox
|
|
||||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
|
||||||
checked={useHashV1}
|
|
||||||
styles={{
|
|
||||||
text: { fontSize: 12 },
|
|
||||||
checkbox: { width: 12, height: 12 },
|
|
||||||
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
|
||||||
}}
|
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
|
|
||||||
useHashV1CheckboxOnChange(isChecked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text variant="small">
|
|
||||||
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the created
|
|
||||||
container will use a legacy partitioning scheme that supports partition key values of size only up to 101
|
|
||||||
bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
|
|
||||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
AnalyticalStorageContent,
|
|
||||||
isSynapseLinkEnabled,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
|
||||||
|
|
||||||
export interface AddMVAnalyticalStoreComponentProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
enableAnalyticalStore: boolean;
|
|
||||||
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
export const AddMVAnalyticalStoreComponent = (props: AddMVAnalyticalStoreComponentProps): JSX.Element => {
|
|
||||||
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
|
|
||||||
|
|
||||||
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
|
|
||||||
if (checked && !enableAnalyticalStore) {
|
|
||||||
setEnableAnalyticalStore(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
|
|
||||||
if (checked && enableAnalyticalStore) {
|
|
||||||
setEnableAnalyticalStore(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
{AnalyticalStorageContent()}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
|
||||||
<div role="radiogroup">
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={enableAnalyticalStore}
|
|
||||||
disabled={!isSynapseLinkEnabled()}
|
|
||||||
aria-label="Enable analytical store"
|
|
||||||
aria-checked={enableAnalyticalStore}
|
|
||||||
name="analyticalStore"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="enableAnalyticalStoreBtn"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">On</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={!enableAnalyticalStore}
|
|
||||||
disabled={!isSynapseLinkEnabled()}
|
|
||||||
aria-label="Disable analytical store"
|
|
||||||
aria-checked={!enableAnalyticalStore}
|
|
||||||
name="analyticalStore"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="disableAnalyticalStoreBtn"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Off</span>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Text variant="small">
|
|
||||||
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
|
|
||||||
Enable Synapse Link for this Cosmos DB account.{" "}
|
|
||||||
<Link
|
|
||||||
href="https://aka.ms/cosmosdb-synapselink"
|
|
||||||
target="_blank"
|
|
||||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
|
||||||
className="capacitycalculator-link"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
<DefaultButton
|
|
||||||
text="Enable"
|
|
||||||
onClick={() => explorer.openEnableSynapseLinkDialog()}
|
|
||||||
style={{ height: 27, width: 80 }}
|
|
||||||
styles={{ label: { fontSize: 12 } }}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
|
||||||
import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels";
|
|
||||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
|
||||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface AddMVFullTextSearchComponentProps {
|
|
||||||
fullTextPolicy: FullTextPolicy;
|
|
||||||
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
|
|
||||||
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
|
|
||||||
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
export const AddMVFullTextSearchComponent = (props: AddMVFullTextSearchComponentProps): JSX.Element => {
|
|
||||||
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Container Full Text Search Policy"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
|
||||||
<FullTextPoliciesComponent
|
|
||||||
fullTextPolicy={fullTextPolicy}
|
|
||||||
onFullTextPathChange={(
|
|
||||||
fullTextPolicy: FullTextPolicy,
|
|
||||||
fullTextIndexes: FullTextIndex[],
|
|
||||||
fullTextPolicyValidated: boolean,
|
|
||||||
) => {
|
|
||||||
setFullTextPolicy(fullTextPolicy);
|
|
||||||
setFullTextIndexes(fullTextIndexes);
|
|
||||||
setFullTextPolicyValidated(fullTextPolicyValidated);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import {
|
|
||||||
getPartitionKeyName,
|
|
||||||
getPartitionKeyPlaceHolder,
|
|
||||||
getPartitionKeyTooltipText,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface AddMVPartitionKeyComponentProps {
|
|
||||||
partitionKey?: string;
|
|
||||||
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
subPartitionKeys: string[];
|
|
||||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
useHashV1: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVPartitionKeyComponent = (props: AddMVPartitionKeyComponentProps): JSX.Element => {
|
|
||||||
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
|
|
||||||
|
|
||||||
const partitionKeyValueOnChange = (value: string): void => {
|
|
||||||
if (!partitionKey && !value.startsWith("/")) {
|
|
||||||
setPartitionKey("/" + value);
|
|
||||||
} else {
|
|
||||||
setPartitionKey(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
|
|
||||||
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
|
|
||||||
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
|
|
||||||
updatedSubPartitionKeys[index] = "/" + value.trim();
|
|
||||||
} else {
|
|
||||||
updatedSubPartitionKeys[index] = value.trim();
|
|
||||||
}
|
|
||||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Partition key
|
|
||||||
</Text>
|
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="addmaterializedView-partitionKeyValue"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
placeholder={getPartitionKeyPlaceHolder()}
|
|
||||||
aria-label={getPartitionKeyName()}
|
|
||||||
pattern=".*"
|
|
||||||
value={partitionKey}
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
partitionKeyValueOnChange(event.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
|
|
||||||
return (
|
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "20px",
|
|
||||||
border: "solid",
|
|
||||||
borderWidth: "0px 0px 1px 1px",
|
|
||||||
marginRight: "5px",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="addMaterializedView-partitionKeyValue"
|
|
||||||
key={`addMaterializedView-partitionKeyValue_${subPartitionKeyIndex}`}
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
size={40}
|
|
||||||
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
|
|
||||||
className="panelTextField"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
|
|
||||||
aria-label={getPartitionKeyName()}
|
|
||||||
pattern={".*"}
|
|
||||||
title={""}
|
|
||||||
value={subPartitionKey}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Delete" }}
|
|
||||||
style={{ height: 27 }}
|
|
||||||
onClick={() => {
|
|
||||||
const updatedSubPartitionKeys = subPartitionKeys.filter(
|
|
||||||
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
|
|
||||||
);
|
|
||||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<DefaultButton
|
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
|
||||||
hidden={useHashV1}
|
|
||||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
|
||||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
|
||||||
>
|
|
||||||
Add hierarchical partition key
|
|
||||||
</DefaultButton>
|
|
||||||
{subPartitionKeys.length > 0 && (
|
|
||||||
<Text variant="small">
|
|
||||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to partition your
|
|
||||||
data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview
|
|
||||||
JavaScript V3 SDK.{" "}
|
|
||||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { Checkbox, Stack } from "@fluentui/react";
|
|
||||||
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
|
|
||||||
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import React from "react";
|
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
|
||||||
|
|
||||||
export interface AddMVThroughputComponentProps {
|
|
||||||
enableDedicatedThroughput: boolean;
|
|
||||||
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isSelectedSourceContainerSharedThroughput: () => boolean;
|
|
||||||
showCollectionThroughputInput: () => boolean;
|
|
||||||
materializedViewThroughputOnChange: (materializedViewThroughputValue: number) => void;
|
|
||||||
isMaterializedViewAutoscaleOnChange: (isMaterializedViewAutoscaleValue: boolean) => void;
|
|
||||||
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): JSX.Element => {
|
|
||||||
const {
|
|
||||||
enableDedicatedThroughput,
|
|
||||||
setEnabledDedicatedThroughput,
|
|
||||||
isSelectedSourceContainerSharedThroughput,
|
|
||||||
showCollectionThroughputInput,
|
|
||||||
materializedViewThroughputOnChange,
|
|
||||||
isMaterializedViewAutoscaleOnChange,
|
|
||||||
setIsThroughputCapExceeded,
|
|
||||||
isCostAknowledgedOnChange,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
|
|
||||||
<Stack horizontal verticalAlign="center">
|
|
||||||
<Checkbox
|
|
||||||
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
|
||||||
checked={enableDedicatedThroughput}
|
|
||||||
styles={{
|
|
||||||
text: { fontSize: 12 },
|
|
||||||
checkbox: { width: 12, height: 12 },
|
|
||||||
label: { padding: 0, alignItems: "center" },
|
|
||||||
}}
|
|
||||||
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{showCollectionThroughputInput() && (
|
|
||||||
<ThroughputInput
|
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
|
|
||||||
isDatabase={false}
|
|
||||||
isSharded={false}
|
|
||||||
isFreeTier={isFreeTierAccount()}
|
|
||||||
isQuickstart={false}
|
|
||||||
setThroughputValue={(throughput: number) => {
|
|
||||||
materializedViewThroughputOnChange(throughput);
|
|
||||||
}}
|
|
||||||
setIsAutoscale={(isAutoscale: boolean) => {
|
|
||||||
isMaterializedViewAutoscaleOnChange(isAutoscale);
|
|
||||||
}}
|
|
||||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
|
|
||||||
setIsThroughputCapExceeded(isThroughputCapExceeded);
|
|
||||||
}}
|
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
|
||||||
isCostAknowledgedOnChange(isAcknowledged);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { ActionButton, IconButton, Stack } from "@fluentui/react";
|
|
||||||
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
|
|
||||||
export interface AddMVUniqueKeysComponentProps {
|
|
||||||
uniqueKeys: string[];
|
|
||||||
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVUniqueKeysComponent = (props: AddMVUniqueKeysComponentProps): JSX.Element => {
|
|
||||||
const { uniqueKeys, setUniqueKeys } = props;
|
|
||||||
|
|
||||||
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
|
|
||||||
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
|
|
||||||
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return uniqueKey;
|
|
||||||
});
|
|
||||||
setUniqueKeys(updatedUniqueKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
|
|
||||||
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
|
|
||||||
setUniqueKeys(updatedUniqueKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addUniqueKeyOnClick = (): void => {
|
|
||||||
setUniqueKeys([...uniqueKeys, ""]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
{UniqueKeysHeader()}
|
|
||||||
|
|
||||||
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder={
|
|
||||||
userContext.apiType === "Mongo"
|
|
||||||
? "Comma separated paths e.g. firstName,address.zipCode"
|
|
||||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
|
||||||
}
|
|
||||||
className="panelTextField"
|
|
||||||
autoFocus
|
|
||||||
value={uniqueKey}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Delete" }}
|
|
||||||
style={{ height: 27 }}
|
|
||||||
onClick={() => {
|
|
||||||
deleteUniqueKeyOnClick(uniqueKeyIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
iconProps={{ iconName: "Add" }}
|
|
||||||
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
|
|
||||||
onClick={() => {
|
|
||||||
addUniqueKeyOnClick();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add unique key
|
|
||||||
</ActionButton>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
|
||||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
|
||||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
|
||||||
import {
|
|
||||||
ContainerVectorPolicyTooltipContent,
|
|
||||||
scrollToSection,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface AddMVVectorSearchComponentProps {
|
|
||||||
vectorEmbeddingPolicy: VectorEmbedding[];
|
|
||||||
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
|
|
||||||
vectorIndexingPolicy: VectorIndex[];
|
|
||||||
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
|
||||||
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddMVVectorSearchComponent = (props: AddMVVectorSearchComponentProps): JSX.Element => {
|
|
||||||
const {
|
|
||||||
vectorEmbeddingPolicy,
|
|
||||||
setVectorEmbeddingPolicy,
|
|
||||||
vectorIndexingPolicy,
|
|
||||||
setVectorIndexingPolicy,
|
|
||||||
setVectorPolicyValidated,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<CollapsibleSectionComponent
|
|
||||||
title="Container Vector Policy"
|
|
||||||
isExpandedByDefault={false}
|
|
||||||
onExpand={() => {
|
|
||||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
|
||||||
}}
|
|
||||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
|
||||||
>
|
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
|
||||||
<VectorEmbeddingPoliciesComponent
|
|
||||||
vectorEmbeddings={vectorEmbeddingPolicy}
|
|
||||||
vectorIndexes={vectorIndexingPolicy}
|
|
||||||
onVectorEmbeddingChange={(
|
|
||||||
vectorEmbeddingPolicy: VectorEmbedding[],
|
|
||||||
vectorIndexingPolicy: VectorIndex[],
|
|
||||||
vectorPolicyValidated: boolean,
|
|
||||||
) => {
|
|
||||||
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
|
|
||||||
setVectorIndexingPolicy(vectorIndexingPolicy);
|
|
||||||
setVectorPolicyValidated(vectorPolicyValidated);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</CollapsibleSectionComponent>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
import {
|
|
||||||
DirectionalHint,
|
|
||||||
Dropdown,
|
|
||||||
DropdownMenuItemType,
|
|
||||||
Icon,
|
|
||||||
IDropdownOption,
|
|
||||||
Link,
|
|
||||||
Separator,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TooltipHost,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import { createMaterializedView } from "Common/dataAccess/createMaterializedView";
|
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
|
||||||
import * as DataModels from "Contracts/DataModels";
|
|
||||||
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
|
||||||
import { Collection, Database } from "Contracts/ViewModels";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
AllPropertiesIndexed,
|
|
||||||
FullTextPolicyDefault,
|
|
||||||
getPartitionKey,
|
|
||||||
isSynapseLinkEnabled,
|
|
||||||
parseUniqueKeys,
|
|
||||||
scrollToSection,
|
|
||||||
shouldShowAnalyticalStoreOptions,
|
|
||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
|
||||||
import {
|
|
||||||
chooseSourceContainerStyle,
|
|
||||||
chooseSourceContainerStyles,
|
|
||||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanelStyles";
|
|
||||||
import { AddMVAdvancedComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent";
|
|
||||||
import { AddMVAnalyticalStoreComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent";
|
|
||||||
import { AddMVFullTextSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent";
|
|
||||||
import { AddMVPartitionKeyComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVPartitionKeyComponent";
|
|
||||||
import { AddMVThroughputComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent";
|
|
||||||
import { AddMVUniqueKeysComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVUniqueKeysComponent";
|
|
||||||
import { AddMVVectorSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVVectorSearchComponent";
|
|
||||||
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
|
|
||||||
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
|
|
||||||
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { CollectionCreation } from "Shared/Constants";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
|
||||||
|
|
||||||
export interface AddMaterializedViewPanelProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
sourceContainer?: Collection;
|
|
||||||
}
|
|
||||||
export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): JSX.Element => {
|
|
||||||
const { explorer, sourceContainer } = props;
|
|
||||||
|
|
||||||
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
|
|
||||||
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
|
|
||||||
const [materializedViewId, setMaterializedViewId] = useState<string>();
|
|
||||||
const [definition, setDefinition] = useState<string>();
|
|
||||||
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
|
|
||||||
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
|
|
||||||
const [useHashV1, setUseHashV1] = useState<boolean>();
|
|
||||||
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
|
|
||||||
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
|
|
||||||
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
|
|
||||||
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
|
|
||||||
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
|
|
||||||
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
|
|
||||||
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>();
|
|
||||||
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
|
|
||||||
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>();
|
|
||||||
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>();
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
|
||||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sourceContainerOptions: IDropdownOption[] = [];
|
|
||||||
useDatabases.getState().databases.forEach((database: Database) => {
|
|
||||||
sourceContainerOptions.push({
|
|
||||||
key: database.rid,
|
|
||||||
text: database.id(),
|
|
||||||
itemType: DropdownMenuItemType.Header,
|
|
||||||
});
|
|
||||||
|
|
||||||
database.collections().forEach((collection: Collection) => {
|
|
||||||
const isMaterializedView: boolean = !!collection.materializedViewDefinition();
|
|
||||||
sourceContainerOptions.push({
|
|
||||||
key: collection.rid,
|
|
||||||
text: collection.id(),
|
|
||||||
disabled: isMaterializedView,
|
|
||||||
...(isMaterializedView && {
|
|
||||||
title: "This is a materialized view.",
|
|
||||||
}),
|
|
||||||
data: collection,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setSourceContainerOptions(sourceContainerOptions);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
scrollToSection("panelContainer");
|
|
||||||
}, [errorMessage]);
|
|
||||||
|
|
||||||
let materializedViewThroughput: number;
|
|
||||||
let isMaterializedViewAutoscale: boolean;
|
|
||||||
let isCostAcknowledged: boolean;
|
|
||||||
|
|
||||||
const materializedViewThroughputOnChange = (materializedViewThroughputValue: number): void => {
|
|
||||||
materializedViewThroughput = materializedViewThroughputValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMaterializedViewAutoscaleOnChange = (isMaterializedViewAutoscaleValue: boolean): void => {
|
|
||||||
isMaterializedViewAutoscale = isMaterializedViewAutoscaleValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
|
|
||||||
isCostAcknowledged = isCostAcknowledgedValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelectedSourceContainerSharedThroughput = (): boolean => {
|
|
||||||
if (!selectedSourceContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!selectedSourceContainer.getDatabase().offer();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showCollectionThroughputInput = (): boolean => {
|
|
||||||
if (isServerlessAccount()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableDedicatedThroughput) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showVectorSearchParameters = (): boolean => {
|
|
||||||
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
|
||||||
};
|
|
||||||
|
|
||||||
const showFullTextSearchParameters = (): boolean => {
|
|
||||||
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnalyticalStorageTtl = (): number => {
|
|
||||||
if (!isSynapseLinkEnabled()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldShowAnalyticalStoreOptions()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableAnalyticalStore) {
|
|
||||||
// TODO: always default to 90 days once the backend hotfix is deployed
|
|
||||||
return userContext.features.ttl90Days
|
|
||||||
? Constants.AnalyticalStorageTtl.Days90
|
|
||||||
: Constants.AnalyticalStorageTtl.Infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Constants.AnalyticalStorageTtl.Disabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateInputs = (): boolean => {
|
|
||||||
if (!selectedSourceContainer) {
|
|
||||||
setErrorMessage("Please select a source container");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (materializedViewThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
|
||||||
const errorMessage = isMaterializedViewAutoscale
|
|
||||||
? "Please acknowledge the estimated monthly spend."
|
|
||||||
: "Please acknowledge the estimated daily spend.";
|
|
||||||
setErrorMessage(errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (materializedViewThroughput > CollectionCreation.MaxRUPerPartition) {
|
|
||||||
setErrorMessage("Unsharded collections support up to 10,000 RUs");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showVectorSearchParameters()) {
|
|
||||||
if (!vectorPolicyValidated) {
|
|
||||||
setErrorMessage("Please fix errors in container vector policy");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fullTextPolicyValidated) {
|
|
||||||
setErrorMessage("Please fix errors in container full text search policy");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
|
||||||
event?.preventDefault();
|
|
||||||
|
|
||||||
if (!validateInputs()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const materializedViewIdTrimmed: string = materializedViewId.trim();
|
|
||||||
|
|
||||||
const materializedViewDefinition: DataModels.MaterializedViewDefinition = {
|
|
||||||
sourceCollectionId: sourceContainer.id(),
|
|
||||||
definition: definition,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyTrimmed: string = partitionKey.trim();
|
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys);
|
|
||||||
const partitionKeyVersion = useHashV1 ? undefined : 2;
|
|
||||||
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
|
|
||||||
? {
|
|
||||||
paths: [
|
|
||||||
partitionKeyTrimmed,
|
|
||||||
...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []),
|
|
||||||
],
|
|
||||||
kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
|
||||||
version: partitionKeyVersion,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed;
|
|
||||||
let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy;
|
|
||||||
|
|
||||||
if (showVectorSearchParameters()) {
|
|
||||||
indexingPolicy.vectorIndexes = vectorIndexingPolicy;
|
|
||||||
vectorEmbeddingPolicyFinal = {
|
|
||||||
vectorEmbeddings: vectorEmbeddingPolicy,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showFullTextSearchParameters()) {
|
|
||||||
indexingPolicy.fullTextIndexes = fullTextIndexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const telemetryData: TelemetryProcessor.TelemetryData = {
|
|
||||||
database: {
|
|
||||||
id: selectedSourceContainer.databaseId,
|
|
||||||
shared: isSelectedSourceContainerSharedThroughput(),
|
|
||||||
},
|
|
||||||
collection: {
|
|
||||||
id: materializedViewIdTrimmed,
|
|
||||||
throughput: materializedViewThroughput,
|
|
||||||
isAutoscale: isMaterializedViewAutoscale,
|
|
||||||
partitionKeyPaths,
|
|
||||||
uniqueKeyPolicy,
|
|
||||||
collectionWithDedicatedThroughput: enableDedicatedThroughput,
|
|
||||||
},
|
|
||||||
subscriptionQuotaId: userContext.quotaId,
|
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
|
||||||
};
|
|
||||||
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
|
|
||||||
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
|
|
||||||
|
|
||||||
let offerThroughput: number;
|
|
||||||
let autoPilotMaxThroughput: number;
|
|
||||||
|
|
||||||
if (!databaseLevelThroughput) {
|
|
||||||
if (isMaterializedViewAutoscale) {
|
|
||||||
autoPilotMaxThroughput = materializedViewThroughput;
|
|
||||||
} else {
|
|
||||||
offerThroughput = materializedViewThroughput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createMaterializedViewParams: DataModels.CreateMaterializedViewsParams = {
|
|
||||||
materializedViewId: materializedViewIdTrimmed,
|
|
||||||
materializedViewDefinition: materializedViewDefinition,
|
|
||||||
databaseId: selectedSourceContainer.databaseId,
|
|
||||||
databaseLevelThroughput: databaseLevelThroughput,
|
|
||||||
offerThroughput: offerThroughput,
|
|
||||||
autoPilotMaxThroughput: autoPilotMaxThroughput,
|
|
||||||
analyticalStorageTtl: getAnalyticalStorageTtl(),
|
|
||||||
indexingPolicy: indexingPolicy,
|
|
||||||
partitionKey: partitionKeyPaths,
|
|
||||||
uniqueKeyPolicy: uniqueKeyPolicy,
|
|
||||||
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
|
|
||||||
fullTextPolicy: fullTextPolicy,
|
|
||||||
};
|
|
||||||
|
|
||||||
setIsExecuting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createMaterializedView(createMaterializedViewParams);
|
|
||||||
await explorer.refreshAllDatabases();
|
|
||||||
TelemetryProcessor.traceSuccess(Action.CreateMaterializedView, telemetryData, startKey);
|
|
||||||
useSidePanel.getState().closeSidePanel();
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage: string = getErrorMessage(error);
|
|
||||||
setErrorMessage(errorMessage);
|
|
||||||
setShowErrorDetails(true);
|
|
||||||
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
|
||||||
TelemetryProcessor.traceFailure(Action.CreateMaterializedView, failureTelemetryData, startKey);
|
|
||||||
} finally {
|
|
||||||
setIsExecuting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="panelFormWrapper" id="panelMaterializedView" onSubmit={submit}>
|
|
||||||
{errorMessage && (
|
|
||||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
|
||||||
)}
|
|
||||||
<div className="panelMainContent">
|
|
||||||
<Stack>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Source container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Dropdown
|
|
||||||
placeholder="Choose source container"
|
|
||||||
options={sourceContainerOptions}
|
|
||||||
defaultSelectedKey={sourceContainer?.rid}
|
|
||||||
styles={chooseSourceContainerStyles()}
|
|
||||||
style={chooseSourceContainerStyle()}
|
|
||||||
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
|
|
||||||
/>
|
|
||||||
<Separator className="panelSeparator" />
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
View container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
id="materializedViewId"
|
|
||||||
type="text"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
|
||||||
placeholder={`e.g., viewByEmailId`}
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
value={materializedViewId}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setMaterializedViewId(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Materialized View Definition
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={
|
|
||||||
<Link
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
|
||||||
target="blank"
|
|
||||||
>
|
|
||||||
Learn more about defining materialized views.
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
id="materializedViewDefinition"
|
|
||||||
type="text"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder={"SELECT c.email, c.accountId FROM c"}
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
value={definition || ""}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
|
|
||||||
/>
|
|
||||||
<AddMVPartitionKeyComponent
|
|
||||||
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
|
|
||||||
/>
|
|
||||||
<AddMVThroughputComponent
|
|
||||||
{...{
|
|
||||||
enableDedicatedThroughput,
|
|
||||||
setEnabledDedicatedThroughput,
|
|
||||||
isSelectedSourceContainerSharedThroughput,
|
|
||||||
showCollectionThroughputInput,
|
|
||||||
materializedViewThroughputOnChange,
|
|
||||||
isMaterializedViewAutoscaleOnChange,
|
|
||||||
setIsThroughputCapExceeded,
|
|
||||||
isCostAknowledgedOnChange,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AddMVUniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
|
||||||
<AddMVAnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
|
|
||||||
)}
|
|
||||||
{showVectorSearchParameters() && (
|
|
||||||
<AddMVVectorSearchComponent
|
|
||||||
{...{
|
|
||||||
vectorEmbeddingPolicy,
|
|
||||||
setVectorEmbeddingPolicy,
|
|
||||||
vectorIndexingPolicy,
|
|
||||||
setVectorIndexingPolicy,
|
|
||||||
vectorPolicyValidated,
|
|
||||||
setVectorPolicyValidated,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showFullTextSearchParameters() && (
|
|
||||||
<AddMVFullTextSearchComponent
|
|
||||||
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AddMVAdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
|
||||||
{isExecuting && <PanelLoadingScreen />}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
|
|
||||||
import { CSSProperties } from "react";
|
|
||||||
|
|
||||||
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
|
|
||||||
return {
|
|
||||||
title: { height: 27, lineHeight: 27 },
|
|
||||||
dropdownItem: { fontSize: 12 },
|
|
||||||
dropdownItemDisabled: { fontSize: 12 },
|
|
||||||
dropdownItemSelected: { fontSize: 12 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function chooseSourceContainerStyle(): CSSProperties {
|
|
||||||
return { width: 300, fontSize: 12 };
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`AddMaterializedViewPanel render default panel 1`] = `
|
|
||||||
<form
|
|
||||||
className="panelFormWrapper"
|
|
||||||
id="panelMaterializedView"
|
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="panelMainContent"
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mandatoryStar"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
<Text
|
|
||||||
className="panelTextBold"
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
Source container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Dropdown
|
|
||||||
onChange={[Function]}
|
|
||||||
placeholder="Choose source container"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"fontSize": 12,
|
|
||||||
"width": 300,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styles={
|
|
||||||
{
|
|
||||||
"dropdownItem": {
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
"dropdownItemDisabled": {
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
"dropdownItemSelected": {
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"height": 27,
|
|
||||||
"lineHeight": 27,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Separator
|
|
||||||
className="panelSeparator"
|
|
||||||
/>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mandatoryStar"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
<Text
|
|
||||||
className="panelTextBold"
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
View container id
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
aria-required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
className="panelTextField"
|
|
||||||
id="materializedViewId"
|
|
||||||
onChange={[Function]}
|
|
||||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
|
||||||
placeholder="e.g., viewByEmailId"
|
|
||||||
required={true}
|
|
||||||
size={40}
|
|
||||||
title="May not end with space nor contain characters '\\' '/' '#' '?'"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mandatoryStar"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
<Text
|
|
||||||
className="panelTextBold"
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
Materialized View Definition
|
|
||||||
</Text>
|
|
||||||
<StyledTooltipHostBase
|
|
||||||
content={
|
|
||||||
<StyledLinkBase
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
|
||||||
target="blank"
|
|
||||||
>
|
|
||||||
Learn more about defining materialized views.
|
|
||||||
</StyledLinkBase>
|
|
||||||
}
|
|
||||||
directionalHint={4}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className="panelInfoIcon"
|
|
||||||
iconName="Info"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
</StyledTooltipHostBase>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
aria-required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
className="panelTextField"
|
|
||||||
id="materializedViewDefinition"
|
|
||||||
onChange={[Function]}
|
|
||||||
placeholder="SELECT c.email, c.accountId FROM c"
|
|
||||||
required={true}
|
|
||||||
size={40}
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<AddMVPartitionKeyComponent
|
|
||||||
partitionKey=""
|
|
||||||
setPartitionKey={[Function]}
|
|
||||||
setSubPartitionKeys={[Function]}
|
|
||||||
subPartitionKeys={[]}
|
|
||||||
/>
|
|
||||||
<AddMVThroughputComponent
|
|
||||||
isCostAknowledgedOnChange={[Function]}
|
|
||||||
isMaterializedViewAutoscaleOnChange={[Function]}
|
|
||||||
isSelectedSourceContainerSharedThroughput={[Function]}
|
|
||||||
materializedViewThroughputOnChange={[Function]}
|
|
||||||
setEnabledDedicatedThroughput={[Function]}
|
|
||||||
setIsThroughputCapExceeded={[Function]}
|
|
||||||
showCollectionThroughputInput={[Function]}
|
|
||||||
/>
|
|
||||||
<AddMVUniqueKeysComponent
|
|
||||||
setUniqueKeys={[Function]}
|
|
||||||
uniqueKeys={[]}
|
|
||||||
/>
|
|
||||||
<AddMVAnalyticalStoreComponent
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setEnableAnalyticalStore={[Function]}
|
|
||||||
/>
|
|
||||||
<AddMVAdvancedComponent
|
|
||||||
setSubPartitionKeys={[Function]}
|
|
||||||
setUseHashV1={[Function]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<PanelFooterComponent
|
|
||||||
buttonLabel="OK"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
|
|||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
||||||
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
|
|||||||
@@ -13,15 +13,9 @@ 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 { MaterializedViewsLabels } from "Common/Constants";
|
|
||||||
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 {
|
|
||||||
AddMaterializedViewPanel,
|
|
||||||
AddMaterializedViewPanelProps,
|
|
||||||
} 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,25 +162,6 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMaterializedViewsEnabled()) {
|
|
||||||
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
|
||||||
explorer,
|
|
||||||
};
|
|
||||||
|
|
||||||
actions.push({
|
|
||||||
id: "new_materialized_view",
|
|
||||||
label: MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
icon: <Add16Regular />,
|
|
||||||
onClick: () =>
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
MaterializedViewsLabels.NewMaterializedView,
|
|
||||||
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}, [explorer]);
|
}, [explorer]);
|
||||||
|
|
||||||
|
|||||||
@@ -1028,6 +1028,7 @@ 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);
|
||||||
@@ -1063,9 +1064,14 @@ 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);
|
||||||
|
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
Action.UpdateDocument,
|
Action.UpdateDocument,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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";
|
||||||
@@ -42,7 +43,11 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
return (
|
return (
|
||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
screenshot={FirewallRuleScreenshot}
|
screenshot={
|
||||||
|
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
||||||
|
? VcoreFirewallRuleScreenshot
|
||||||
|
: FirewallRuleScreenshot
|
||||||
|
}
|
||||||
shellName={this.getShellNameForDisplay(this.kind)}
|
shellName={this.getShellNameForDisplay(this.kind)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
public usageSizeInKB: ko.Observable<number>;
|
public usageSizeInKB: ko.Observable<number>;
|
||||||
public computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
public computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||||
public materializedViews: ko.Observable<DataModels.MaterializedView[]>;
|
|
||||||
public materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
|
|
||||||
|
|
||||||
public offer: ko.Observable<DataModels.Offer>;
|
public offer: ko.Observable<DataModels.Offer>;
|
||||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
@@ -126,8 +124,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.requestSchema = data.requestSchema;
|
this.requestSchema = data.requestSchema;
|
||||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||||
this.computedProperties = ko.observable(data.computedProperties);
|
this.computedProperties = ko.observable(data.computedProperties);
|
||||||
this.materializedViews = ko.observable(data.materializedViews);
|
|
||||||
this.materializedViewDefinition = ko.observable(data.materializedViewDefinition);
|
|
||||||
|
|
||||||
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
|
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
|
||||||
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
|
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
|||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { AddCollectionPanel } from "../Panes/AddCollectionPanel/AddCollectionPanel";
|
import { AddCollectionPanel } from "../Panes/AddCollectionPanel";
|
||||||
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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 {
|
||||||
@@ -61,7 +60,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 as Collection)
|
? createResourceTokenTreeNodes(resourceTokenCollection)
|
||||||
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
||||||
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -72,7 +72,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -145,7 +145,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -264,7 +264,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -369,7 +369,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -442,7 +442,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -546,7 +546,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -696,7 +696,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -760,7 +760,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -787,7 +787,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -841,7 +841,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -895,7 +895,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -953,7 +953,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -974,7 +974,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1010,7 +1010,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1046,7 +1046,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1208,7 +1208,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1311,7 +1311,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1445,7 +1445,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1625,7 +1625,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1799,7 +1799,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1897,7 +1897,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2031,7 +2031,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2211,7 +2211,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2266,7 +2266,7 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ jest.mock("Explorer/Tree/Trigger", () => {
|
|||||||
jest.mock("Common/DatabaseAccountUtility", () => {
|
jest.mock("Common/DatabaseAccountUtility", () => {
|
||||||
return {
|
return {
|
||||||
isPublicInternetAccessAllowed: () => true,
|
isPublicInternetAccessAllowed: () => true,
|
||||||
isMaterializedViewsEnabled: () => false,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,15 +134,6 @@ const baseCollection = {
|
|||||||
kind: "hash",
|
kind: "hash",
|
||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
materializedViews: ko.observable<DataModels.MaterializedView[]>([
|
|
||||||
{ id: "view1", _rid: "rid1" },
|
|
||||||
{ id: "view2", _rid: "rid2" },
|
|
||||||
]),
|
|
||||||
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
|
|
||||||
definition: "SELECT * FROM c WHERE c.id = 1",
|
|
||||||
sourceCollectionId: "source1",
|
|
||||||
sourceCollectionRid: "rid123",
|
|
||||||
}),
|
|
||||||
storedProcedures: ko.observableArray([]),
|
storedProcedures: ko.observableArray([]),
|
||||||
userDefinedFunctions: ko.observableArray([]),
|
userDefinedFunctions: ko.observableArray([]),
|
||||||
triggers: ko.observableArray([]),
|
triggers: ko.observableArray([]),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons";
|
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
@@ -29,7 +29,6 @@ export const shouldShowScriptNodes = (): boolean => {
|
|||||||
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 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 +80,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
|
|||||||
return [updatedSampleTree];
|
return [updatedSampleTree];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createResourceTokenTreeNodes = (collection: ViewModels.Collection): TreeNode[] => {
|
export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => {
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -111,7 +110,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.Collection):
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children,
|
children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
iconSrc: TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
@@ -229,7 +228,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() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
iconSrc: TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
|
|||||||
@@ -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 } from "p-retry";
|
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
@@ -35,21 +35,26 @@ 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: promiseRetry.Options = {
|
private retryOptions: 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +275,17 @@ export class PhoenixClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializeCancelEventListener(): void {
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
this.abortSignal = this.abortController.signal;
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||||
|
if (event.ctrlKey && (event.key === "c" || event.key === "z")) {
|
||||||
|
this.abortController.abort(new AbortError("Request canceled"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
||||||
const errInfo = jsonData;
|
const errInfo = jsonData;
|
||||||
switch (errInfo?.type) {
|
switch (errInfo?.type) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import promiseRetry, { AbortError } from "p-retry";
|
import promiseRetry, { AbortError, Options } 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: promiseRetry.Options;
|
private retryOptions: Options;
|
||||||
private translationFunction: TFunction;
|
private translationFunction: TFunction;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
|||||||
@@ -197,6 +197,11 @@ 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 = {
|
||||||
@@ -237,7 +242,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: undefined, billingCurrency: undefined };
|
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,6 +291,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||||
return undefined;
|
return new Map<string, Map<string, string>>();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -227,11 +227,13 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
|||||||
let costPerHour = 0;
|
let 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;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
export enum Action {
|
export enum Action {
|
||||||
CollapseTreeNode,
|
CollapseTreeNode,
|
||||||
CreateCollection,
|
CreateCollection,
|
||||||
CreateMaterializedView,
|
|
||||||
CreateDocument,
|
CreateDocument,
|
||||||
CreateStoredProcedure,
|
CreateStoredProcedure,
|
||||||
CreateTrigger,
|
CreateTrigger,
|
||||||
@@ -120,7 +119,6 @@ export enum Action {
|
|||||||
NotebooksGalleryPublishedCount,
|
NotebooksGalleryPublishedCount,
|
||||||
SelfServe,
|
SelfServe,
|
||||||
ExpandAddCollectionPaneAdvancedSection,
|
ExpandAddCollectionPaneAdvancedSection,
|
||||||
ExpandAddMaterializedViewPaneAdvancedSection,
|
|
||||||
SchemaAnalyzerClickAnalyze,
|
SchemaAnalyzerClickAnalyze,
|
||||||
SelfServeComponent,
|
SelfServeComponent,
|
||||||
LaunchQuickstart,
|
LaunchQuickstart,
|
||||||
|
|||||||
@@ -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("cosmosuser@");
|
return content?.includes("cosmosshelluser@");
|
||||||
}
|
}
|
||||||
|
|
||||||
private isMongoShellStarted(content: string | undefined) {
|
private isMongoShellStarted(content: string | undefined) {
|
||||||
@@ -68,7 +68,6 @@ 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) {
|
||||||
@@ -114,6 +113,13 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ 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", () => {
|
||||||
@@ -89,6 +96,18 @@ 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()", () => {
|
||||||
@@ -201,18 +220,6 @@ 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,
|
||||||
@@ -225,5 +232,52 @@ 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, {}]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,8 +61,9 @@ export function buildDocumentsQueryPartitionProjections(
|
|||||||
projectedProperty += `[${projection}]`;
|
projectedProperty += `[${projection}]`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const fullAccess = `${collectionAlias}${projectedProperty}`;
|
||||||
projections.push(`${collectionAlias}${projectedProperty}`);
|
const wrappedProjection = `IIF(IS_DEFINED(${fullAccess}), ${fullAccess}, {})`;
|
||||||
|
projections.push(wrappedProjection);
|
||||||
}
|
}
|
||||||
|
|
||||||
return projections.join(",");
|
return projections.join(",");
|
||||||
@@ -130,6 +131,8 @@ export const extractPartitionKeyValues = (
|
|||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
partitionKeyValues.push(value);
|
partitionKeyValues.push(value);
|
||||||
|
} else {
|
||||||
|
partitionKeyValues.push({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user