mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 19:19:56 +00:00
Compare commits
13 Commits
release/ma
...
users/ajpa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a9f8c3e32 | ||
|
|
5a36a6b45d | ||
|
|
32576f50d3 | ||
|
|
10f5a5fbfe | ||
|
|
8eb53674dc | ||
|
|
257256f915 | ||
|
|
41f5401016 | ||
|
|
a4c9a47d4e | ||
|
|
c43132d5c0 | ||
|
|
6ce81099ef | ||
|
|
777e411f4f | ||
|
|
63d4b4f4ef | ||
|
|
eaf9a14e7d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,3 +21,5 @@ GettingStarted-ignore*.ipynb
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/.vs/cosmos-explorer
|
||||
/.vs/slnx.sqlite-journal
|
||||
|
||||
BIN
.vs/slnx.sqlite
BIN
.vs/slnx.sqlite
Binary file not shown.
@@ -1914,13 +1914,20 @@ input::-webkit-calendar-picker-indicator::after {
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
height: 32px;
|
||||
background-color: #f2f2f2;
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
margin-bottom: -0.5px;
|
||||
|
||||
li {
|
||||
// Override the bootstrap defaults here to align with our layout constants.
|
||||
margin-bottom: 0px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
package-lock.json
generated
51
package-lock.json
generated
@@ -86,7 +86,7 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.6.2",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
@@ -12662,7 +12662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sanitize-html": {
|
||||
@@ -21799,6 +21801,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
|
||||
"integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
@@ -30243,14 +30257,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
|
||||
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"@types/retry": "0.12.2",
|
||||
"is-network-error": "^1.0.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
@@ -35997,6 +36017,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"dev": true,
|
||||
@@ -36044,6 +36071,20 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"dev": true,
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.6.2",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
|
||||
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.";
|
||||
}
|
||||
|
||||
export class MaterializedViewsLabels {
|
||||
public static readonly NewMaterializedView: string = "New Materialized View";
|
||||
}
|
||||
export class FeedbackLabels {
|
||||
public static readonly provideFeedback: string = "Provide feedback";
|
||||
}
|
||||
|
||||
@@ -26,7 +26,3 @@ export function getWorkloadType(): 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}`);
|
||||
}
|
||||
|
||||
// TO DO: Remove when we get RP API Spec with materializedViews
|
||||
/* 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;
|
||||
});
|
||||
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export interface DatabaseAccountExtendedProperties {
|
||||
writeLocations?: DatabaseAccountResponseLocation[];
|
||||
enableFreeTier?: boolean;
|
||||
enableAnalyticalStorage?: boolean;
|
||||
enableMaterializedViews?: boolean;
|
||||
isVirtualNetworkFilterEnabled?: boolean;
|
||||
ipRules?: IpRule[];
|
||||
privateEndpointConnections?: unknown[];
|
||||
@@ -165,8 +164,6 @@ export interface Collection extends Resource {
|
||||
schema?: ISchema;
|
||||
requestSchema?: () => void;
|
||||
computedProperties?: ComputedProperties;
|
||||
materializedViews?: MaterializedView[];
|
||||
materializedViewDefinition?: MaterializedViewDefinition;
|
||||
}
|
||||
|
||||
export interface CollectionsWithPagination {
|
||||
@@ -226,17 +223,6 @@ export interface ComputedProperty {
|
||||
|
||||
export type ComputedProperties = ComputedProperty[];
|
||||
|
||||
export interface MaterializedView {
|
||||
id: string;
|
||||
_rid: string;
|
||||
}
|
||||
|
||||
export interface MaterializedViewDefinition {
|
||||
definition: string;
|
||||
sourceCollectionId: string;
|
||||
sourceCollectionRid?: string;
|
||||
}
|
||||
|
||||
export interface PartitionKey {
|
||||
paths: string[];
|
||||
kind: "Hash" | "Range" | "MultiHash";
|
||||
@@ -359,7 +345,9 @@ export interface CreateDatabaseParams {
|
||||
offerThroughput?: number;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParamsBase {
|
||||
export interface CreateCollectionParams {
|
||||
createNewDatabase: boolean;
|
||||
collectionId: string;
|
||||
databaseId: string;
|
||||
databaseLevelThroughput: boolean;
|
||||
offerThroughput?: number;
|
||||
@@ -373,16 +361,6 @@ export interface CreateCollectionParamsBase {
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
||||
createNewDatabase: boolean;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
||||
materializedViewId: string;
|
||||
materializedViewDefinition: MaterializedViewDefinition;
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicy {
|
||||
vectorEmbeddings: VectorEmbedding[];
|
||||
}
|
||||
|
||||
@@ -143,8 +143,6 @@ export interface Collection extends CollectionBase {
|
||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||
documentIds: ko.ObservableArray<DocumentId>;
|
||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
|
||||
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
|
||||
|
||||
cassandraKeys: CassandraTableKeys;
|
||||
cassandraSchema: CassandraTableKey[];
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { MaterializedViewsLabels } from "Common/Constants";
|
||||
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import {
|
||||
AddMaterializedViewPanel,
|
||||
AddMaterializedViewPanelProps,
|
||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,10 +45,6 @@ import {
|
||||
ConflictResolutionComponentProps,
|
||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||
import {
|
||||
MaterializedViewComponent,
|
||||
MaterializedViewComponentProps,
|
||||
} from "./SettingsSubComponents/MaterializedViewComponent";
|
||||
import {
|
||||
MongoIndexingPolicyComponent,
|
||||
MongoIndexingPolicyComponentProps,
|
||||
@@ -166,7 +162,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private shouldShowComputedPropertiesEditor: boolean;
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
private shouldShowPartitionKeyEditor: boolean;
|
||||
private isMaterializedView: boolean;
|
||||
private isVectorSearchEnabled: boolean;
|
||||
private isFullTextSearchEnabled: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
@@ -184,8 +179,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||
this.isMaterializedView =
|
||||
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !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(),
|
||||
};
|
||||
|
||||
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
||||
collection: this.collection,
|
||||
explorer: this.props.settingsTab.getContainer(),
|
||||
};
|
||||
|
||||
const tabs: SettingsV2TabInfo[] = [];
|
||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||
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 = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
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,
|
||||
ContainerVectorPolicyTab,
|
||||
ThroughputBucketsTab,
|
||||
MaterializedViewTab,
|
||||
}
|
||||
|
||||
export enum ContainerPolicyTabTypes {
|
||||
@@ -172,8 +171,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
return "Container Policies";
|
||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||
return "Throughput Buckets";
|
||||
case SettingsV2TabTypes.MaterializedViewTab:
|
||||
return "Materialized Views (Preview)";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
|
||||
@@ -48,15 +48,6 @@ export const collection = {
|
||||
]),
|
||||
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
||||
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: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -60,8 +60,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
"indexingPolicy": [Function],
|
||||
"materializedViewDefinition": [Function],
|
||||
"materializedViews": [Function],
|
||||
"offer": [Function],
|
||||
"partitionKey": {
|
||||
"kind": "hash",
|
||||
@@ -141,8 +139,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
"indexingPolicy": [Function],
|
||||
"materializedViewDefinition": [Function],
|
||||
"materializedViews": [Function],
|
||||
"offer": [Function],
|
||||
"partitionKey": {
|
||||
"kind": "hash",
|
||||
@@ -262,8 +258,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
"indexingPolicy": [Function],
|
||||
"materializedViewDefinition": [Function],
|
||||
"materializedViews": [Function],
|
||||
"offer": [Function],
|
||||
"partitionKey": {
|
||||
"kind": "hash",
|
||||
@@ -342,101 +336,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
shouldDiscardComputedProperties={false}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
|
||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||
@@ -19,7 +19,7 @@ export class NotebookContainerClient {
|
||||
private clearReconnectionAttemptMessage? = () => {};
|
||||
private isResettingWorkspace: boolean;
|
||||
private phoenixClient: PhoenixClient;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private retryOptions: Options;
|
||||
private scheduleTimerId: NodeJS.Timeout;
|
||||
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import Explorer from "../Explorer";
|
||||
import { AddCollectionPanel } from "./AddCollectionPanel";
|
||||
|
||||
const props = {
|
||||
@@ -21,25 +21,11 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import {
|
||||
AllPropertiesIndexed,
|
||||
AnalyticalStorageContent,
|
||||
ContainerVectorPolicyTooltipContent,
|
||||
FullTextPolicyDefault,
|
||||
getPartitionKey,
|
||||
getPartitionKeyName,
|
||||
getPartitionKeyPlaceHolder,
|
||||
getPartitionKeyTooltipText,
|
||||
isFreeTierAccount,
|
||||
isSynapseLinkEnabled,
|
||||
parseUniqueKeys,
|
||||
scrollToSection,
|
||||
SharedDatabaseDefault,
|
||||
shouldShowAnalyticalStoreOptions,
|
||||
UniqueKeysHeader,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
FullTextPoliciesComponent,
|
||||
getFullTextLanguageOptions,
|
||||
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
@@ -56,15 +42,16 @@ import {
|
||||
isVectorSearchEnabled,
|
||||
} from "Utils/CapabilityUtils";
|
||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import "../../Controls/ThroughputInput/ThroughputInput.less";
|
||||
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
import "../Controls/ThroughputInput/ThroughputInput.less";
|
||||
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
||||
|
||||
export interface AddCollectionPanelProps {
|
||||
explorer: Explorer;
|
||||
@@ -72,6 +59,40 @@ export interface AddCollectionPanelProps {
|
||||
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 = {
|
||||
vectorEmbeddings: [],
|
||||
};
|
||||
@@ -124,7 +145,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: getPartitionKey(props.isQuickstart),
|
||||
partitionKey: this.getPartitionKey(),
|
||||
subPartitionKeys: [],
|
||||
enableDedicatedThroughput: false,
|
||||
createMongoWildCardIndex:
|
||||
@@ -140,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
vectorEmbeddingPolicy: [],
|
||||
vectorIndexingPolicy: [],
|
||||
vectorPolicyValidated: true,
|
||||
fullTextPolicy: FullTextPolicyDefault,
|
||||
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
|
||||
fullTextIndexes: [],
|
||||
fullTextPolicyValidated: true,
|
||||
};
|
||||
@@ -154,7 +175,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
||||
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
||||
scrollToSection("panelContainer");
|
||||
this.scrollToSection("panelContainer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +192,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
)}
|
||||
|
||||
{!this.state.errorMessage && isFreeTierAccount() && (
|
||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||
<PanelInfoErrorComponent
|
||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||
messageType="info"
|
||||
@@ -331,8 +352,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
required
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
@@ -379,10 +400,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
@@ -439,8 +460,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
@@ -559,14 +580,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{getPartitionKeyName()}
|
||||
{this.getPartitionKeyName()}
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getPartitionKeyTooltipText()}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={getPartitionKeyTooltipText()}
|
||||
ariaLabel={this.getPartitionKeyTooltipText()}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -580,8 +604,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
required
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
placeholder={getPartitionKeyPlaceHolder()}
|
||||
aria-label={getPartitionKeyName()}
|
||||
placeholder={this.getPartitionKeyPlaceHolder()}
|
||||
aria-label={this.getPartitionKeyName()}
|
||||
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
||||
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
||||
value={this.state.partitionKey}
|
||||
@@ -619,8 +643,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={index > 0 ? 1 : 0}
|
||||
className="panelTextField"
|
||||
autoComplete="off"
|
||||
placeholder={getPartitionKeyPlaceHolder(index)}
|
||||
aria-label={getPartitionKeyName()}
|
||||
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
||||
aria-label={this.getPartitionKeyName()}
|
||||
pattern={".*"}
|
||||
title={""}
|
||||
value={subPartitionKey}
|
||||
@@ -711,10 +735,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{this.shouldShowCollectionThroughputInput() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||
@@ -729,7 +753,27 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<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 => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
||||
@@ -777,10 +821,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{shouldShowAnalyticalStoreOptions() && (
|
||||
{this.shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{AnalyticalStorageContent()}
|
||||
{this.getAnalyticalStorageContent()}
|
||||
</Text>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
@@ -788,7 +832,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-checked={this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
@@ -803,7 +847,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-checked={!this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
@@ -817,7 +861,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!isSynapseLinkEnabled() && (
|
||||
{!this.isSynapseLinkEnabled() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small">
|
||||
Azure Synapse Link is required for creating an analytical store{" "}
|
||||
@@ -847,9 +891,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
title="Container Vector Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
}}
|
||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
@@ -875,7 +919,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
title="Container Full Text Search Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
this.scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
}}
|
||||
//TODO: uncomment when learn more text becomes available
|
||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||
@@ -903,7 +947,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||
scrollToSection("collapsibleAdvancedSectionContent");
|
||||
this.scrollToSection("collapsibleAdvancedSectionContent");
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||
@@ -1013,6 +1057,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 {
|
||||
if (event.target.checked && !this.state.createNewDatabase) {
|
||||
this.setState({
|
||||
@@ -1100,12 +1169,48 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return !!selectedDatabase?.offer();
|
||||
}
|
||||
|
||||
private isFreeTierAccount(): boolean {
|
||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||
}
|
||||
|
||||
private getFreeTierIndexingText(): string {
|
||||
return this.state.enableIndexing
|
||||
? "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.";
|
||||
}
|
||||
|
||||
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 {
|
||||
if (
|
||||
userContext.features.partitionKeyDefault &&
|
||||
@@ -1117,6 +1222,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
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
|
||||
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
||||
// return (
|
||||
@@ -1147,7 +1280,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||
if (!isFreeTierAccount()) {
|
||||
if (!this.isFreeTierAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1156,6 +1289,39 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
: 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() {
|
||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||
}
|
||||
@@ -1236,11 +1402,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private getAnalyticalStorageTtl(): number {
|
||||
if (!isSynapseLinkEnabled()) {
|
||||
if (!this.isSynapseLinkEnabled()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!shouldShowAnalyticalStoreOptions()) {
|
||||
if (!this.shouldShowAnalyticalStoreOptions()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1254,6 +1420,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return Constants.AnalyticalStorageTtl.Disabled;
|
||||
}
|
||||
|
||||
private scrollToSection(id: string): void {
|
||||
document.getElementById(id)?.scrollIntoView();
|
||||
}
|
||||
|
||||
private getSampleDBName(): string {
|
||||
const existingSampleDBs = useDatabases
|
||||
.getState()
|
||||
@@ -1288,7 +1458,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
partitionKeyString = "/'$pk'";
|
||||
}
|
||||
|
||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
|
||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||
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,5 +1,6 @@
|
||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
@@ -204,8 +205,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
type="text"
|
||||
aria-required="true"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
size={40}
|
||||
aria-label={databaseIdLabel}
|
||||
placeholder={databaseIdPlaceHolder}
|
||||
|
||||
@@ -39,7 +39,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
data-lpignore={true}
|
||||
id="database-id"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
styles={
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -7,6 +7,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
@@ -202,8 +203,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
styles={getTextFieldStyles()}
|
||||
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Type a new keyspace id"
|
||||
size={40}
|
||||
value={newKeyspaceId}
|
||||
@@ -292,8 +293,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
required={true}
|
||||
ariaLabel="addCollection-table Id Create table"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter table Id"
|
||||
size={20}
|
||||
value={tableId}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -235,8 +236,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
|
||||
@@ -93,7 +93,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
id="newDatabaseId"
|
||||
name="newDatabaseId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
||||
placeholder="Type a new database id"
|
||||
required={true}
|
||||
size={40}
|
||||
@@ -178,7 +178,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
id="collectionId"
|
||||
name="collectionId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
||||
placeholder="e.g., Container1"
|
||||
required={true}
|
||||
size={40}
|
||||
@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||
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 { useDatabases } from "Explorer/useDatabases";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
|
||||
@@ -13,15 +13,9 @@ import {
|
||||
SplitButton,
|
||||
} from "@fluentui/react-components";
|
||||
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 Explorer from "Explorer/Explorer";
|
||||
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import {
|
||||
AddMaterializedViewPanel,
|
||||
AddMaterializedViewPanelProps,
|
||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||
import { Tabs } from "Explorer/Tabs/Tabs";
|
||||
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
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;
|
||||
}, [explorer]);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
||||
import NewDocumentIcon from "../../../../images/NewDocument.svg";
|
||||
import UploadIcon from "../../../../images/Upload_16x16.svg";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||
@@ -131,6 +132,14 @@ export const useDocumentsTabStyles = makeStyles({
|
||||
backgroundColor: "white",
|
||||
zIndex: 1,
|
||||
},
|
||||
refreshBtn: {
|
||||
position: "absolute",
|
||||
top: "3px",
|
||||
right: "4px",
|
||||
float: "right",
|
||||
zIndex: 1,
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
deleteProgressContent: {
|
||||
paddingTop: tokens.spacingVerticalL,
|
||||
},
|
||||
@@ -2144,6 +2153,18 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||
/>
|
||||
</div>
|
||||
{tableContainerSizePx?.width >= calculateOffset(selectedColumnIds.length) + 200 && (
|
||||
<div
|
||||
title="Refresh"
|
||||
className={styles.refreshBtn}
|
||||
role="button"
|
||||
onClick={() => refreshDocumentsGrid(false)}
|
||||
aria-label="Refresh"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img src={RefreshIcon} alt="Refresh" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{tableItems.length > 0 && (
|
||||
<a
|
||||
|
||||
@@ -233,7 +233,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
aria-label="Select column"
|
||||
size="small"
|
||||
icon={<MoreHorizontalRegular />}
|
||||
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||
style={{ position: "absolute", right: 10, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
<MenuPopover>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -57,7 +58,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
}
|
||||
|
||||
this.id = editable.observable<string>();
|
||||
this.id.validations([ScriptTabBase._isValidId]);
|
||||
this.id.validations([IsValidCosmosDbResourceId]);
|
||||
|
||||
this.editorContent = editable.observable<string>();
|
||||
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
|
||||
@@ -262,29 +263,6 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
private static _isValidId(id: string): boolean {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidStartCharacters = /^[/?#\\]/;
|
||||
if (invalidStartCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||
if (invalidMiddleCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||
if (invalidEndCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _isNotEmpty(value: string): boolean {
|
||||
return !!value;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { Pivot, PivotItem } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React from "react";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -455,11 +456,12 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const isValidId: boolean = event.currentTarget.reportValidity();
|
||||
if (this.state.saveButton.visible) {
|
||||
this.setState({
|
||||
id: event.target.value,
|
||||
saveButton: {
|
||||
enabled: true,
|
||||
enabled: isValidId,
|
||||
visible: this.props.scriptTabBaseInstance.isNew(),
|
||||
},
|
||||
discardButton: {
|
||||
@@ -528,8 +530,8 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
className="formTree"
|
||||
type="text"
|
||||
required
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
aria-label="Stored procedure id"
|
||||
placeholder="Enter the new stored procedure id"
|
||||
size={40}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
import VcoreFirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -42,7 +43,11 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
return (
|
||||
<QuickstartFirewallNotification
|
||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||
screenshot={FirewallRuleScreenshot}
|
||||
screenshot={
|
||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
||||
? VcoreFirewallRuleScreenshot
|
||||
: FirewallRuleScreenshot
|
||||
}
|
||||
shellName={this.getShellNameForDisplay(this.kind)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React, { Component } from "react";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
@@ -192,29 +193,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
});
|
||||
}
|
||||
|
||||
private isValidId(id: string): boolean {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidStartCharacters = /^[/?#\\]/;
|
||||
if (invalidStartCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||
if (invalidMiddleCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||
if (invalidEndCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isNotEmpty(value: string): boolean {
|
||||
return !!value;
|
||||
}
|
||||
@@ -286,7 +264,13 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string,
|
||||
): void => {
|
||||
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
||||
let isValidId: boolean = true;
|
||||
if (inputElement) {
|
||||
isValidId = inputElement.reportValidity();
|
||||
}
|
||||
|
||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
||||
this.setState({ triggerId: newValue });
|
||||
};
|
||||
|
||||
@@ -313,7 +297,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
autoFocus
|
||||
required
|
||||
type="text"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new trigger id"
|
||||
size={40}
|
||||
value={triggerId}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { Label, TextField } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React, { Component } from "react";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
@@ -64,7 +65,13 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string,
|
||||
): void => {
|
||||
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
||||
let isValidId: boolean = true;
|
||||
if (inputElement) {
|
||||
isValidId = inputElement.reportValidity();
|
||||
}
|
||||
|
||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
||||
this.setState({ udfId: newValue });
|
||||
};
|
||||
|
||||
@@ -238,29 +245,6 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
});
|
||||
}
|
||||
|
||||
private isValidId(id: string): boolean {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidStartCharacters = /^[/?#\\]/;
|
||||
if (invalidStartCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||
if (invalidMiddleCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||
if (invalidEndCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isNotEmpty(value: string): boolean {
|
||||
return !!value;
|
||||
}
|
||||
@@ -284,7 +268,8 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
required
|
||||
readOnly={!isUdfIdEditable}
|
||||
type="text"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new user defined function id"
|
||||
size={40}
|
||||
value={udfId}
|
||||
|
||||
@@ -58,8 +58,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
public usageSizeInKB: ko.Observable<number>;
|
||||
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 conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||
@@ -126,8 +124,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.requestSchema = data.requestSchema;
|
||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||
this.computedProperties = ko.observable(data.computedProperties);
|
||||
this.materializedViews = ko.observable(data.materializedViews);
|
||||
this.materializedViewDefinition = ko.observable(data.materializedViewDefinition);
|
||||
|
||||
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
|
||||
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import Explorer from "../Explorer";
|
||||
import { AddCollectionPanel } from "../Panes/AddCollectionPanel/AddCollectionPanel";
|
||||
import { AddCollectionPanel } from "../Panes/AddCollectionPanel";
|
||||
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
||||
import { Home16Regular } from "@fluentui/react-icons";
|
||||
import { AuthType } from "AuthType";
|
||||
import { Collection } from "Contracts/ViewModels";
|
||||
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
||||
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import {
|
||||
@@ -61,7 +60,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
||||
|
||||
const databaseTreeNodes = useMemo(() => {
|
||||
return userContext.authType === AuthType.ResourceToken
|
||||
? createResourceTokenTreeNodes(resourceTokenCollection as Collection)
|
||||
? createResourceTokenTreeNodes(resourceTokenCollection)
|
||||
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
||||
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -72,7 +72,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -145,7 +145,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -264,7 +264,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -369,7 +369,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -442,7 +442,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -546,7 +546,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -696,7 +696,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -760,7 +760,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -787,7 +787,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -841,7 +841,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -895,7 +895,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -953,7 +953,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -974,7 +974,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1010,7 +1010,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1046,7 +1046,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1208,7 +1208,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1311,7 +1311,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1445,7 +1445,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1625,7 +1625,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1799,7 +1799,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -1897,7 +1897,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -2031,7 +2031,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -2211,7 +2211,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
@@ -2266,7 +2266,7 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
|
||||
},
|
||||
],
|
||||
"className": "collectionNode",
|
||||
"iconSrc": <EyeRegular
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
|
||||
@@ -82,7 +82,6 @@ jest.mock("Explorer/Tree/Trigger", () => {
|
||||
jest.mock("Common/DatabaseAccountUtility", () => {
|
||||
return {
|
||||
isPublicInternetAccessAllowed: () => true,
|
||||
isMaterializedViewsEnabled: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -135,15 +134,6 @@ const baseCollection = {
|
||||
kind: "hash",
|
||||
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([]),
|
||||
userDefinedFunctions: 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 { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||
@@ -29,7 +29,6 @@ export const shouldShowScriptNodes = (): boolean => {
|
||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
||||
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
||||
const MaterializedViewCollectionIcon = <EyeRegular fontSize={16} />; //check icon
|
||||
|
||||
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
||||
const updatedSampleTree: TreeNode = {
|
||||
@@ -81,7 +80,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
|
||||
return [updatedSampleTree];
|
||||
};
|
||||
|
||||
export const createResourceTokenTreeNodes = (collection: ViewModels.Collection): TreeNode[] => {
|
||||
export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => {
|
||||
if (!collection) {
|
||||
return [
|
||||
{
|
||||
@@ -111,7 +110,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.Collection):
|
||||
isExpanded: true,
|
||||
children,
|
||||
className: "collectionNode",
|
||||
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||
iconSrc: TreeCollectionIcon,
|
||||
onClick: () => {
|
||||
// Rewritten version of expandCollapseCollection
|
||||
useSelectedNode.getState().setSelectedNode(collection);
|
||||
@@ -229,7 +228,7 @@ export const buildCollectionNode = (
|
||||
children: children,
|
||||
className: "collectionNode",
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||
iconSrc: TreeCollectionIcon,
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(collection);
|
||||
collection.openTab();
|
||||
|
||||
@@ -1,48 +1,71 @@
|
||||
{
|
||||
"MaterializedViewsBuilderDescription": "Provision a Materializedviews builder cluster for your Azure Cosmos DB account. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"MaterializedViewsBuilder": "Materializedviews Builder",
|
||||
"Provisioned": "Provisioned",
|
||||
"Deprovisioned": "Deprovisioned",
|
||||
"LearnAboutMaterializedViews": "Learn more about materializedviews.",
|
||||
"DeprovisioningDetailsText": "Learn more about materializedviews.",
|
||||
"MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.",
|
||||
"SKUs": "SKUs",
|
||||
"SKUsPlaceHolder": "Select SKUs",
|
||||
"NumberOfInstances": "Number of instances",
|
||||
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
|
||||
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
|
||||
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
|
||||
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
|
||||
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
|
||||
"CreateMessage": "MaterializedViewsBuilder resource is being created.",
|
||||
"CreateInitializeTitle": "Provisioning resource",
|
||||
"CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.",
|
||||
"CreateSuccessTitle": "Resource provisioned",
|
||||
"CreateSuccesseMessage": "Materializedviews Builder resource provisioned.",
|
||||
"CreateFailureTitle": "Failed to provision resource",
|
||||
"CreateFailureMessage": "Materializedviews Builder resource provisioning failed.",
|
||||
"UpdateMessage": "MaterializedViewsBuilder resource is being updated.",
|
||||
"UpdateInitializeTitle": "Updating resource",
|
||||
"UpdateInitializeMessage": "Materializedviews Builder resource will be updated.",
|
||||
"UpdateSuccessTitle": "Resource updated",
|
||||
"UpdateSuccesseMessage": "Materializedviews Builder resource updated.",
|
||||
"UpdateFailureTitle": "Failed to update resource",
|
||||
"UpdateFailureMessage": "Materializedviews Builder resource updation failed.",
|
||||
"DeleteMessage": "MaterializedViewsBuilder resource is being deleted.",
|
||||
"DeleteInitializeTitle": "Deleting resource",
|
||||
"DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.",
|
||||
"DeleteSuccessTitle": "Resource deleted",
|
||||
"DeleteSuccesseMessage": "Materializedviews Builder resource deleted.",
|
||||
"DeleteFailureTitle": "Failed to delete resource",
|
||||
"DeleteFailureMessage": "Materializedviews Builder resource deletion failed.",
|
||||
"ApproximateCost": "Approximate Cost Per Hour",
|
||||
"CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.",
|
||||
"MetricsString": "Metrics",
|
||||
"MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ",
|
||||
"MetricsBlade": "the metrics blade.",
|
||||
"MonitorUsage": "Monitor Usage",
|
||||
"ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ",
|
||||
"ResizingDecisionLink": "learn more about Materializedviews Builder sizing.",
|
||||
"WarningBannerOnUpdate": "Adding or modifying Materializedviews Builder instances may affect your bill.",
|
||||
"WarningBannerOnDelete": "After deprovisioning the Materializedviews Builder, your materializedviews will not be updated with new source changes anymore. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition."
|
||||
"MaterializedViewsBuilderDescription": "Provision a materialized views builder cluster for your Azure Cosmos DB account. Materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"MaterializedViewsBuilder": "Materialized views Builder",
|
||||
"Provisioned": "Provisioned",
|
||||
"Deprovisioned": "Deprovisioned",
|
||||
"LearnAboutMaterializedViews": "Learn more about materialized views.",
|
||||
"DeprovisioningDetailsText": "Learn more about materialized views.",
|
||||
"MaterializedviewsBuilderPricing": "Learn more about materialized views pricing.",
|
||||
"SKUs": "SKUs",
|
||||
"SKUsPlaceHolder": "Select SKUs",
|
||||
"NumberOfInstances": "Number of instances",
|
||||
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
|
||||
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
|
||||
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
|
||||
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
|
||||
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
|
||||
"CreateMessage": "Materialized views builder resource is being created.",
|
||||
"CreateInitializeTitle": "Provisioning resource",
|
||||
"CreateInitializeMessage": "Materialized views Builder resource will be provisioned.",
|
||||
"CreateSuccessTitle": "Resource provisioned",
|
||||
"CreateSuccesseMessage": "Materialized views Builder resource provisioned.",
|
||||
"CreateFailureTitle": "Failed to provision resource",
|
||||
"CreateFailureMessage": "Materialized views Builder resource provisioning failed.",
|
||||
"UpdateMessage": "Materialized views builder resource is being updated.",
|
||||
"UpdateInitializeTitle": "Updating resource",
|
||||
"UpdateInitializeMessage": "Materialized views Builder resource will be updated.",
|
||||
"UpdateSuccessTitle": "Resource updated",
|
||||
"UpdateSuccesseMessage": "Materialized views Builder resource updated.",
|
||||
"UpdateFailureTitle": "Failed to update resource",
|
||||
"UpdateFailureMessage": "Materialized views Builder resource updation failed.",
|
||||
"DeleteMessage": "Materialized views builder resource is being deleted.",
|
||||
"DeleteInitializeTitle": "Deleting resource",
|
||||
"DeleteInitializeMessage": "Materialized views Builder resource will be deleted.",
|
||||
"DeleteSuccessTitle": "Resource deleted",
|
||||
"DeleteSuccesseMessage": "Materialized views Builder resource deleted.",
|
||||
"DeleteFailureTitle": "Failed to delete resource",
|
||||
"DeleteFailureMessage": "Materialized views Builder resource deletion failed.",
|
||||
"ApproximateCost": "Approximate Cost Per Hour",
|
||||
"CostText": "Hourly cost of the materialized views Builder resource depends on the SKU selection and number of instances per region.",
|
||||
"MetricsString": "Metrics",
|
||||
"MetricsText": "Monitor the CPU and memory usage for the materialized views Builder instances in ",
|
||||
"MetricsBlade": "the metrics blade.",
|
||||
"MonitorUsage": "Monitor Usage",
|
||||
"ResizingDecisionText": "To understand if the materialized views Builder is the right size, ",
|
||||
"ResizingDecisionLink": "learn more about materialized views Builder sizing.",
|
||||
"WarningBannerOnUpdate": "Adding or modifying materialized views Builder instances may affect your bill.",
|
||||
"WarningBannerOnDelete": "After deprovisioning the materialized views Builder, your materialized views will not be updated with new source changes anymore. materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"GlobalsecondaryindexesBuilderDescription": "Provision a global secondary indexes builder for your Azure Cosmos DB account. The global secondary indexes builder is compute in your account that performs read operations on source collections for any updates and populates the global secondary indexes as per their definition.",
|
||||
"GlobalsecondaryindexesBuilder": "Global secondary indexes builder",
|
||||
"LearnAboutGlobalSecondaryIndexes": "Learn more about global secondary indexes.",
|
||||
"GlobalsecondaryindexesDeprovisioningDetailsText": "Learn more about global secondary indexes.",
|
||||
"GlobalsecondaryindexesBuilderPricing": "Learn more about global secondary indexes pricing.",
|
||||
"GlobalsecondaryindexesCreateMessage": "Global secondary indexes builder resource is being created.",
|
||||
"GlobalsecondaryindexesCreateInitializeMessage": "Global secondary indexes builder resource will be provisioned.",
|
||||
"GlobalsecondaryindexesCreateSuccesseMessage": "Global secondary indexes builder resource provisioned.",
|
||||
"GlobalsecondaryindexesCreateFailureMessage": "Global secondary indexes builder resource provisioning failed.",
|
||||
"GlobalsecondaryindexesUpdateMessage": "Global secondary indexes builder resource is being updated.",
|
||||
"GlobalsecondaryindexesUpdateInitializeMessage": "Global secondary indexes builder resource will be updated.",
|
||||
"GlobalsecondaryindexesUpdateSuccesseMessage": "Global secondary indexes builder resource updated.",
|
||||
"GlobalsecondaryindexesUpdateFailureMessage": "Global secondary indexes builder resource update failed.",
|
||||
"GlobalsecondaryindexesDeleteMessage": "Global secondary indexes builder resource is being deleted.",
|
||||
"GlobalsecondaryindexesDeleteInitializeMessage": "Global secondary indexes builder resource will be deleted.",
|
||||
"GlobalsecondaryindexesDeleteSuccesseMessage": "Global secondary indexes builder resource deleted.",
|
||||
"GlobalsecondaryindexesDeleteFailureMessage": "Global secondary indexes builder resource deletion failed.",
|
||||
"GlobalsecondaryindexesCostText": "Hourly cost of the global secondary indexes builder resource depends on the SKU selection and number of instances per region.",
|
||||
"GlobalsecondaryindexesMetricsText": "Monitor the CPU and memory usage for the global secondary indexes builder instances in ",
|
||||
"GlobalsecondaryindexesResizingDecisionText": "To understand if the global secondary indexes builder is the right size, ",
|
||||
"GlobalsecondaryindexesesizingDecisionLink": "learn more about global secondary indexes builder sizing.",
|
||||
"GlobalsecondaryindexesWarningBannerOnUpdate": "Adding or modifying global secondary indexes builder instances may affect your bill.",
|
||||
"GlobalsecondaryindexesWarningBannerOnDelete": "After deprovisioning the global secondary indexes builder, your global secondary indexes will no longer be updated with new source changes. Global secondary indexes builder is compute in your account that performs read operations on source collection for any updates and applies them on global secondary indexes as per their definition."
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { userContext } from "UserContext";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
@@ -35,21 +35,26 @@ import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
export class PhoenixClient {
|
||||
private armResourceId: string;
|
||||
private containerHealthHandler: NodeJS.Timeout;
|
||||
private retryOptions: promiseRetry.Options = {
|
||||
private retryOptions: Options = {
|
||||
retries: Notebook.retryAttempts,
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
private abortController: AbortController;
|
||||
private abortSignal: AbortSignal;
|
||||
|
||||
constructor(armResourceId: string) {
|
||||
this.armResourceId = armResourceId;
|
||||
}
|
||||
|
||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||
this.initializeCancelEventListener();
|
||||
|
||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
||||
retries: 4,
|
||||
maxTimeout: 20000,
|
||||
minTimeout: 20000,
|
||||
signal: this.abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,6 +275,17 @@ export class PhoenixClient {
|
||||
};
|
||||
}
|
||||
|
||||
private initializeCancelEventListener(): void {
|
||||
this.abortController = new AbortController();
|
||||
this.abortSignal = this.abortController.signal;
|
||||
|
||||
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey && (event.key === "c" || event.key === "z")) {
|
||||
this.abortController.abort(new AbortError("Request canceled"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
||||
const errInfo = jsonData;
|
||||
switch (errInfo?.type) {
|
||||
|
||||
@@ -6,9 +6,9 @@ import { RefreshResult } from "../SelfServeTypes";
|
||||
import MaterializedViewsBuilder from "./MaterializedViewsBuilder";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
MaterializedViewsBuilderServiceResource,
|
||||
PriceMapAndCurrencyCode,
|
||||
RegionsResponse,
|
||||
MaterializedViewsBuilderServiceResource,
|
||||
UpdateMaterializedViewsBuilderRequestParameters,
|
||||
} from "./MaterializedViewsBuilderTypes";
|
||||
|
||||
@@ -123,11 +123,23 @@ export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<Ref
|
||||
if (response.properties.status === ResourceStatus.Running.toString()) {
|
||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
|
||||
return {
|
||||
isUpdateInProgress: true,
|
||||
updateInProgressMessageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateMessage" : "CreateMessage",
|
||||
};
|
||||
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
|
||||
return {
|
||||
isUpdateInProgress: true,
|
||||
updateInProgressMessageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteMessage" : "DeleteMessage",
|
||||
};
|
||||
} else {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
|
||||
return {
|
||||
isUpdateInProgress: true,
|
||||
updateInProgressMessageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateMessage" : "UpdateMessage",
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
//TODO differentiate between different failures
|
||||
|
||||
@@ -29,17 +29,20 @@ import {
|
||||
updateMaterializedViewsBuilderResource,
|
||||
} from "./MaterializedViewsBuilder.rp";
|
||||
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
const costPerHourDefaultValue: Description = {
|
||||
textTKey: "CostText",
|
||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
const metricsStringValue: Description = {
|
||||
textTKey: "MetricsText",
|
||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesMetricsText" : "MetricsText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: generateBladeLink(BladeType.Metrics),
|
||||
@@ -76,7 +79,8 @@ const onNumberOfInstancesChange = (
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
@@ -116,7 +120,8 @@ const onEnableMaterializedViewsBuilderChange = (
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
@@ -129,10 +134,17 @@ const onEnableMaterializedViewsBuilderChange = (
|
||||
} else {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnDelete",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesWarningBannerOnDelete" : "WarningBannerOnDelete",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||
textTKey: "DeprovisioningDetailsText",
|
||||
href:
|
||||
userContext.apiType === "SQL"
|
||||
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
|
||||
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesDeprovisioningDetailsText"
|
||||
: "DeprovisioningDetailsText",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
@@ -182,18 +194,19 @@ const getInstancesMax = async (): Promise<number> => {
|
||||
};
|
||||
|
||||
const NumberOfInstancesDropdownInfo: Info = {
|
||||
messageTKey: "ResizingDecisionText",
|
||||
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesResizingDecisionText" : "ResizingDecisionText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
|
||||
textTKey: "ResizingDecisionLink",
|
||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesesizingDecisionLink" : "ResizingDecisionLink",
|
||||
},
|
||||
};
|
||||
|
||||
const ApproximateCostDropDownInfo: Info = {
|
||||
messageTKey: "CostText",
|
||||
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -268,15 +281,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "DeleteInitializeTitle",
|
||||
messageTKey: "DeleteInitializeMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesDeleteInitializeMessage"
|
||||
: "DeleteInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "DeleteSuccessTitle",
|
||||
messageTKey: "DeleteSuccesseMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteSuccesseMessage" : "DeleteSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "DeleteFailureTitle",
|
||||
messageTKey: "DeleteFailureMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteFailureMessage" : "DeleteFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -289,15 +307,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "UpdateInitializeTitle",
|
||||
messageTKey: "UpdateInitializeMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesUpdateInitializeMessage"
|
||||
: "UpdateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "UpdateSuccessTitle",
|
||||
messageTKey: "UpdateSuccesseMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateSuccesseMessage" : "UpdateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "UpdateFailureTitle",
|
||||
messageTKey: "UpdateFailureMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateFailureMessage" : "UpdateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -311,15 +334,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "CreateInitializeTitle",
|
||||
messageTKey: "CreateInitializeMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesCreateInitializeMessage"
|
||||
: "CreateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "CreateSuccessTitle",
|
||||
messageTKey: "CreateSuccesseMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateSuccesseMessage" : "CreateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "CreateFailureTitle",
|
||||
messageTKey: "CreateFailureMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateFailureMessage" : "CreateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -366,11 +394,17 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
|
||||
@Values({
|
||||
description: {
|
||||
textTKey: "MaterializedViewsBuilderDescription",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesBuilderDescription"
|
||||
: "MaterializedViewsBuilderDescription",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||
textTKey: "LearnAboutMaterializedViews",
|
||||
href:
|
||||
userContext.apiType === "SQL"
|
||||
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
|
||||
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
|
||||
textTKey: userContext.apiType === "SQL" ? "LearnAboutGlobalSecondaryIndexes" : "LearnAboutMaterializedViews",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -378,7 +412,7 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
|
||||
@OnChange(onEnableMaterializedViewsBuilderChange)
|
||||
@Values({
|
||||
labelTKey: "MaterializedViewsBuilder",
|
||||
labelTKey: userContext.apiType === "SQL" ? "GlobalSecondaryIndexesBuilder" : "MaterializedViewsBuilder",
|
||||
trueLabelTKey: "Provisioned",
|
||||
falseLabelTKey: "Deprovisioned",
|
||||
})
|
||||
|
||||
@@ -11,13 +11,24 @@ import { updateUserContext } from "../UserContext";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import "./SelfServe.less";
|
||||
import { SelfServeComponent } from "./SelfServeComponent";
|
||||
import { SelfServeDescriptor } from "./SelfServeTypes";
|
||||
import { SelfServeBaseClass, SelfServeDescriptor } from "./SelfServeTypes";
|
||||
import { SelfServeType } from "./SelfServeUtils";
|
||||
initializeIcons();
|
||||
|
||||
const loadTranslationFile = async (className: string): Promise<void> => {
|
||||
const loadTranslationFile = async (
|
||||
className: string | SelfServeBaseClass,
|
||||
selfServeType?: SelfServeType,
|
||||
): Promise<void> => {
|
||||
const language = i18n.languages[0];
|
||||
const fileName = `${className}.json`;
|
||||
let namespace: string; // className is used as a key to retrieve the localized strings
|
||||
let fileName: string;
|
||||
if (className instanceof SelfServeBaseClass) {
|
||||
fileName = `${selfServeType}.json`;
|
||||
namespace = className.constructor.name;
|
||||
} else {
|
||||
fileName = `${className}.json`;
|
||||
namespace = className;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let translations: any;
|
||||
@@ -28,12 +39,16 @@ const loadTranslationFile = async (className: string): Promise<void> => {
|
||||
} catch (e) {
|
||||
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`);
|
||||
}
|
||||
i18n.addResourceBundle(language, className, translations.default, true);
|
||||
|
||||
i18n.addResourceBundle(language, namespace, translations.default, true);
|
||||
};
|
||||
|
||||
const loadTranslations = async (className: string): Promise<void> => {
|
||||
const loadTranslations = async (
|
||||
className: string | SelfServeBaseClass,
|
||||
selfServeType: SelfServeType,
|
||||
): Promise<void> => {
|
||||
await loadTranslationFile("Common");
|
||||
await loadTranslationFile(className);
|
||||
await loadTranslationFile(className, selfServeType);
|
||||
};
|
||||
|
||||
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
||||
@@ -41,13 +56,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
case SelfServeType.example: {
|
||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||
const selfServeExample = new SelfServeExample.default();
|
||||
await loadTranslations(selfServeType);
|
||||
await loadTranslations(selfServeExample, selfServeType);
|
||||
return selfServeExample.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.sqlx: {
|
||||
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||
const sqlX = new SqlX.default();
|
||||
await loadTranslations(selfServeType);
|
||||
await loadTranslations(sqlX, selfServeType);
|
||||
return sqlX.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.graphapicompute: {
|
||||
@@ -55,7 +70,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||
);
|
||||
const graphAPICompute = new GraphAPICompute.default();
|
||||
await loadTranslations(selfServeType);
|
||||
await loadTranslations(graphAPICompute, selfServeType);
|
||||
return graphAPICompute.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.materializedviewsbuilder: {
|
||||
@@ -63,7 +78,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||
);
|
||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||
await loadTranslations(selfServeType);
|
||||
await loadTranslations(materializedViewsBuilder, selfServeType);
|
||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { TFunction } from "i18next";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import React from "react";
|
||||
import { WithTranslation } from "react-i18next";
|
||||
import * as _ from "underscore";
|
||||
@@ -80,7 +80,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
private static readonly defaultRetryIntervalInMs = 30000;
|
||||
private smartUiGeneratorClassName: string;
|
||||
private retryIntervalInMs: number;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private retryOptions: Options;
|
||||
private translationFunction: TFunction;
|
||||
|
||||
componentDidMount(): void {
|
||||
|
||||
@@ -197,6 +197,11 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let billingCurrency;
|
||||
for (const region of map.keys()) {
|
||||
// if no offering id is found for that region, skipping calling price API
|
||||
const subMap = map.get(region);
|
||||
if (!subMap || subMap.size === 0) {
|
||||
continue;
|
||||
}
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
const regionShortName = await getRegionShortName(region);
|
||||
const requestBody: OfferingIdRequest = {
|
||||
@@ -237,7 +242,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||
return { priceMap: undefined, billingCurrency: undefined };
|
||||
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,6 +291,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||
return undefined;
|
||||
return new Map<string, Map<string, string>>();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,11 +227,13 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
let costPerHour = 0;
|
||||
let costBreakdown = "";
|
||||
for (const regionItem of regions) {
|
||||
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
|
||||
const incrementalCost = priceMap?.get(regionItem.locationName)?.get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||
} else if (incrementalCost === 0) {
|
||||
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
||||
} else if (currencyCode === undefined) {
|
||||
throw new Error(`Currency code not found in price map.`);
|
||||
}
|
||||
|
||||
let regionalInstanceCount = instanceCount;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
export enum Action {
|
||||
CollapseTreeNode,
|
||||
CreateCollection,
|
||||
CreateMaterializedView,
|
||||
CreateDocument,
|
||||
CreateStoredProcedure,
|
||||
CreateTrigger,
|
||||
@@ -120,7 +119,6 @@ export enum Action {
|
||||
NotebooksGalleryPublishedCount,
|
||||
SelfServe,
|
||||
ExpandAddCollectionPaneAdvancedSection,
|
||||
ExpandAddMaterializedViewPaneAdvancedSection,
|
||||
SchemaAnalyzerClickAnalyze,
|
||||
SelfServeComponent,
|
||||
LaunchQuickstart,
|
||||
|
||||
@@ -17,7 +17,7 @@ export class JupyterLabAppFactory {
|
||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
||||
this.restartShell = true;
|
||||
}
|
||||
return content?.includes("cosmosuser@");
|
||||
return content?.includes("cosmosshelluser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
@@ -68,7 +68,6 @@ export class JupyterLabAppFactory {
|
||||
const session = await manager.startNew();
|
||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||
const content = message.content && message.content[0]?.toString();
|
||||
|
||||
if (this.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
@@ -114,6 +113,13 @@ export class JupyterLabAppFactory {
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
// Close terminal when Ctrl key is pressed
|
||||
term.node.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey) {
|
||||
this.onShellExited(false);
|
||||
}
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ describe("AuthorizationUtils", () => {
|
||||
it("should throw an error if token is malformed", () => {
|
||||
expect(() =>
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
// This is an invalid JWT token used for testing
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.",
|
||||
),
|
||||
).toThrow();
|
||||
@@ -47,6 +48,7 @@ describe("AuthorizationUtils", () => {
|
||||
it("should return decrypted token payload", () => {
|
||||
expect(
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
// This is an expired JWT token used for testing
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ",
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
18
src/Utils/ValidationUtils.test.ts
Normal file
18
src/Utils/ValidationUtils.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
|
||||
|
||||
const testCases = [
|
||||
["validId", true],
|
||||
["forward/slash", false],
|
||||
["back\\slash", false],
|
||||
["question?mark", false],
|
||||
["hash#mark", false],
|
||||
["?invalidstart", false],
|
||||
["invalidEnd/", false],
|
||||
["space-at-end ", false],
|
||||
];
|
||||
|
||||
describe("IsValidCosmosDbResourceId", () => {
|
||||
test.each(testCases)("IsValidCosmosDbResourceId(%p). Expected: %p", (id: string, expected: boolean) => {
|
||||
expect(IsValidCosmosDbResourceId(id)).toBe(expected);
|
||||
});
|
||||
});
|
||||
24
src/Utils/ValidationUtils.ts
Normal file
24
src/Utils/ValidationUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Common methods and constants for validation
|
||||
//
|
||||
|
||||
//
|
||||
// Validation of id for Cosmos DB resources:
|
||||
// - Database
|
||||
// - Container
|
||||
// - Stored Procedure
|
||||
// - User Defined Function (UDF)
|
||||
// - Trigger
|
||||
//
|
||||
// Use these with <input> elements
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
export const ValidCosmosDbIdInputPattern: RegExp = /[^\/?#\\]*[^\/?# \\]/;
|
||||
export const ValidCosmosDbIdDescription: string = "May not end with space nor contain characters '\\' '/' '#' '?'";
|
||||
|
||||
// For a standalone function regex, we need to wrap the previous reg expression,
|
||||
// to test against the entire value. This is done implicitly by input elements.
|
||||
const ValidCosmosDbIdRegex: RegExp = new RegExp(`^(?:${ValidCosmosDbIdInputPattern.source})$`);
|
||||
|
||||
export function IsValidCosmosDbResourceId(id: string): boolean {
|
||||
return id && ValidCosmosDbIdRegex.test(id);
|
||||
}
|
||||
Reference in New Issue
Block a user